mirror of https://github.com/ansible/ansible.git
Migrated to community.crypto
parent
9d2d137038
commit
ef24d794ee
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,364 +0,0 @@
|
|||||||
# -*- 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)$")
|
|
||||||
|
|
||||||
|
|
||||||
def ecs_client_argument_spec():
|
|
||||||
return dict(
|
|
||||||
entrust_api_user=dict(type='str', required=True),
|
|
||||||
entrust_api_key=dict(type='str', required=True, no_log=True),
|
|
||||||
entrust_api_client_cert_path=dict(type='path', required=True),
|
|
||||||
entrust_api_client_cert_key_path=dict(type='path', required=True, no_log=True),
|
|
||||||
entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
}
|
|
||||||
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()
|
|
@ -1 +0,0 @@
|
|||||||
acme_account_info.py
|
|
@ -1,278 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: acme_account
|
|
||||||
author: "Felix Fontein (@felixfontein)"
|
|
||||||
version_added: "2.6"
|
|
||||||
short_description: Create, modify or delete ACME accounts
|
|
||||||
description:
|
|
||||||
- "Allows to create, modify or delete accounts with a CA supporting the
|
|
||||||
L(ACME protocol,https://tools.ietf.org/html/rfc8555),
|
|
||||||
such as L(Let's Encrypt,https://letsencrypt.org/)."
|
|
||||||
- "This module only works with the ACME v2 protocol."
|
|
||||||
notes:
|
|
||||||
- "The M(acme_certificate) module also allows to do basic account management.
|
|
||||||
When using both modules, it is recommended to disable account management
|
|
||||||
for M(acme_certificate). For that, use the C(modify_account) option of
|
|
||||||
M(acme_certificate)."
|
|
||||||
seealso:
|
|
||||||
- name: Automatic Certificate Management Environment (ACME)
|
|
||||||
description: The specification of the ACME protocol (RFC 8555).
|
|
||||||
link: https://tools.ietf.org/html/rfc8555
|
|
||||||
- module: acme_account_info
|
|
||||||
description: Retrieves facts about an ACME account.
|
|
||||||
- module: openssl_privatekey
|
|
||||||
description: Can be used to create a private account key.
|
|
||||||
- module: acme_inspect
|
|
||||||
description: Allows to debug problems.
|
|
||||||
extends_documentation_fragment:
|
|
||||||
- acme
|
|
||||||
options:
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- "The state of the account, to be identified by its account key."
|
|
||||||
- "If the state is C(absent), the account will either not exist or be
|
|
||||||
deactivated."
|
|
||||||
- "If the state is C(changed_key), the account must exist. The account
|
|
||||||
key will be changed; no other information will be touched."
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
choices:
|
|
||||||
- present
|
|
||||||
- absent
|
|
||||||
- changed_key
|
|
||||||
allow_creation:
|
|
||||||
description:
|
|
||||||
- "Whether account creation is allowed (when state is C(present))."
|
|
||||||
type: bool
|
|
||||||
default: yes
|
|
||||||
contact:
|
|
||||||
description:
|
|
||||||
- "A list of contact URLs."
|
|
||||||
- "Email addresses must be prefixed with C(mailto:)."
|
|
||||||
- "See U(https://tools.ietf.org/html/rfc8555#section-7.3)
|
|
||||||
for what is allowed."
|
|
||||||
- "Must be specified when state is C(present). Will be ignored
|
|
||||||
if state is C(absent) or C(changed_key)."
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
default: []
|
|
||||||
terms_agreed:
|
|
||||||
description:
|
|
||||||
- "Boolean indicating whether you agree to the terms of service document."
|
|
||||||
- "ACME servers can require this to be true."
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
new_account_key_src:
|
|
||||||
description:
|
|
||||||
- "Path to a file containing the ACME account RSA or Elliptic Curve key to change to."
|
|
||||||
- "Same restrictions apply as to C(account_key_src)."
|
|
||||||
- "Mutually exclusive with C(new_account_key_content)."
|
|
||||||
- "Required if C(new_account_key_content) is not used and state is C(changed_key)."
|
|
||||||
type: path
|
|
||||||
new_account_key_content:
|
|
||||||
description:
|
|
||||||
- "Content of the ACME account RSA or Elliptic Curve key to change to."
|
|
||||||
- "Same restrictions apply as to C(account_key_content)."
|
|
||||||
- "Mutually exclusive with C(new_account_key_src)."
|
|
||||||
- "Required if C(new_account_key_src) is not used and state is C(changed_key)."
|
|
||||||
type: str
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = '''
|
|
||||||
- name: Make sure account exists and has given contacts. We agree to TOS.
|
|
||||||
acme_account:
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
state: present
|
|
||||||
terms_agreed: yes
|
|
||||||
contact:
|
|
||||||
- mailto:me@example.com
|
|
||||||
- mailto:myself@example.org
|
|
||||||
|
|
||||||
- name: Make sure account has given email address. Don't create account if it doesn't exist
|
|
||||||
acme_account:
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
state: present
|
|
||||||
allow_creation: no
|
|
||||||
contact:
|
|
||||||
- mailto:me@example.com
|
|
||||||
|
|
||||||
- name: Change account's key to the one stored in the variable new_account_key
|
|
||||||
acme_account:
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
new_account_key_content: '{{ new_account_key }}'
|
|
||||||
state: changed_key
|
|
||||||
|
|
||||||
- name: Delete account (we have to use the new key)
|
|
||||||
acme_account:
|
|
||||||
account_key_content: '{{ new_account_key }}'
|
|
||||||
state: absent
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
account_uri:
|
|
||||||
description: ACME account URI, or None if account does not exist.
|
|
||||||
returned: always
|
|
||||||
type: str
|
|
||||||
'''
|
|
||||||
|
|
||||||
from ansible.module_utils.acme import (
|
|
||||||
ModuleFailException,
|
|
||||||
ACMEAccount,
|
|
||||||
handle_standard_module_arguments,
|
|
||||||
get_default_argspec,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
|
|
||||||
|
|
||||||
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),
|
|
||||||
))
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
handle_standard_module_arguments(module, needs_acme_v2=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
account = ACMEAccount(module)
|
|
||||||
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'] = 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 = account.send_signed_request(account.uri, payload)
|
|
||||||
if info['status'] != 200:
|
|
||||||
raise ModuleFailException('Error deactivating account: {0} {1}'.format(info['status'], result))
|
|
||||||
changed = True
|
|
||||||
elif state == 'present':
|
|
||||||
allow_creation = module.params.get('allow_creation')
|
|
||||||
# Make sure contact is a list of strings (unfortunately, Ansible doesn't do that for us)
|
|
||||||
contact = [str(v) for v in module.params.get('contact')]
|
|
||||||
terms_agreed = module.params.get('terms_agreed')
|
|
||||||
created, account_data = account.setup_account(
|
|
||||||
contact,
|
|
||||||
terms_agreed=terms_agreed,
|
|
||||||
allow_creation=allow_creation,
|
|
||||||
)
|
|
||||||
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'] = 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'] = account.key_data['jwk']
|
|
||||||
elif state == 'changed_key':
|
|
||||||
# Parse new account key
|
|
||||||
error, new_key_data = account.parse_key(
|
|
||||||
module.params.get('new_account_key_src'),
|
|
||||||
module.params.get('new_account_key_content')
|
|
||||||
)
|
|
||||||
if error:
|
|
||||||
raise ModuleFailException("error while parsing account key: %s" % error)
|
|
||||||
# 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'] = 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 = account.directory['keyChange']
|
|
||||||
protected = {
|
|
||||||
"alg": new_key_data['alg'],
|
|
||||||
"jwk": new_key_data['jwk'],
|
|
||||||
"url": url,
|
|
||||||
}
|
|
||||||
payload = {
|
|
||||||
"account": account.uri,
|
|
||||||
"newKey": new_key_data['jwk'], # specified in draft 12 and older
|
|
||||||
"oldKey": account.jwk, # specified in draft 13 and newer
|
|
||||||
}
|
|
||||||
data = account.sign_request(protected, payload, new_key_data)
|
|
||||||
# Send request and verify result
|
|
||||||
result, info = account.send_signed_request(url, data)
|
|
||||||
if info['status'] != 200:
|
|
||||||
raise ModuleFailException('Error account key rollover: {0} {1}'.format(info['status'], result))
|
|
||||||
if module._diff:
|
|
||||||
account.key_data = new_key_data
|
|
||||||
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': 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)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,301 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2018 Felix Fontein <felix@fontein.de>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: acme_account_info
|
|
||||||
author: "Felix Fontein (@felixfontein)"
|
|
||||||
version_added: "2.7"
|
|
||||||
short_description: Retrieves information on ACME accounts
|
|
||||||
description:
|
|
||||||
- "Allows to retrieve information on accounts a CA supporting the
|
|
||||||
L(ACME protocol,https://tools.ietf.org/html/rfc8555),
|
|
||||||
such as L(Let's Encrypt,https://letsencrypt.org/)."
|
|
||||||
- "This module only works with the ACME v2 protocol."
|
|
||||||
notes:
|
|
||||||
- "The M(acme_account) module allows to modify, create and delete ACME accounts."
|
|
||||||
- "This module was called C(acme_account_facts) before Ansible 2.8. The usage
|
|
||||||
did not change."
|
|
||||||
options:
|
|
||||||
retrieve_orders:
|
|
||||||
description:
|
|
||||||
- "Whether to retrieve the list of order URLs or order objects, if provided
|
|
||||||
by the ACME server."
|
|
||||||
- "A value of C(ignore) will not fetch the list of orders."
|
|
||||||
- "Currently, Let's Encrypt does not return orders, so the C(orders) result
|
|
||||||
will always be empty."
|
|
||||||
type: str
|
|
||||||
choices:
|
|
||||||
- ignore
|
|
||||||
- url_list
|
|
||||||
- object_list
|
|
||||||
default: ignore
|
|
||||||
version_added: "2.9"
|
|
||||||
seealso:
|
|
||||||
- module: acme_account
|
|
||||||
description: Allows to create, modify or delete an ACME account.
|
|
||||||
extends_documentation_fragment:
|
|
||||||
- acme
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = '''
|
|
||||||
- name: Check whether an account with the given account key exists
|
|
||||||
acme_account_info:
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
register: account_data
|
|
||||||
- name: Verify that account exists
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_data.exists
|
|
||||||
- name: Print account URI
|
|
||||||
debug: var=account_data.account_uri
|
|
||||||
- name: Print account contacts
|
|
||||||
debug: var=account_data.account.contact
|
|
||||||
|
|
||||||
- name: Check whether the account exists and is accessible with the given account key
|
|
||||||
acme_account_info:
|
|
||||||
account_key_content: "{{ acme_account_key }}"
|
|
||||||
account_uri: "{{ acme_account_uri }}"
|
|
||||||
register: account_data
|
|
||||||
- name: Verify that account exists
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_data.exists
|
|
||||||
- name: Print account contacts
|
|
||||||
debug: var=account_data.account.contact
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
exists:
|
|
||||||
description: Whether the account exists.
|
|
||||||
returned: always
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
account_uri:
|
|
||||||
description: ACME account URI, or None if account does not exist.
|
|
||||||
returned: always
|
|
||||||
type: str
|
|
||||||
|
|
||||||
account:
|
|
||||||
description: The account information, as retrieved from the ACME server.
|
|
||||||
returned: if account exists
|
|
||||||
type: dict
|
|
||||||
contains:
|
|
||||||
contact:
|
|
||||||
description: the challenge resource that must be created for validation
|
|
||||||
returned: always
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
sample: "['mailto:me@example.com', 'tel:00123456789']"
|
|
||||||
status:
|
|
||||||
description: the account's status
|
|
||||||
returned: always
|
|
||||||
type: str
|
|
||||||
choices: ['valid', 'deactivated', 'revoked']
|
|
||||||
sample: valid
|
|
||||||
orders:
|
|
||||||
description:
|
|
||||||
- A URL where a list of orders can be retrieved for this account.
|
|
||||||
- Use the I(retrieve_orders) option to query this URL and retrieve the
|
|
||||||
complete list of orders.
|
|
||||||
returned: always
|
|
||||||
type: str
|
|
||||||
sample: https://example.ca/account/1/orders
|
|
||||||
public_account_key:
|
|
||||||
description: the public account key as a L(JSON Web Key,https://tools.ietf.org/html/rfc7517).
|
|
||||||
returned: always
|
|
||||||
type: str
|
|
||||||
sample: '{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"}'
|
|
||||||
|
|
||||||
orders:
|
|
||||||
description:
|
|
||||||
- "The list of orders."
|
|
||||||
- "If I(retrieve_orders) is C(url_list), this will be a list of URLs."
|
|
||||||
- "If I(retrieve_orders) is C(object_list), this will be a list of objects."
|
|
||||||
type: list
|
|
||||||
#elements: ... depends on retrieve_orders
|
|
||||||
returned: if account exists, I(retrieve_orders) is not C(ignore), and server supports order listing
|
|
||||||
contains:
|
|
||||||
status:
|
|
||||||
description: The order's status.
|
|
||||||
type: str
|
|
||||||
choices:
|
|
||||||
- pending
|
|
||||||
- ready
|
|
||||||
- processing
|
|
||||||
- valid
|
|
||||||
- invalid
|
|
||||||
expires:
|
|
||||||
description:
|
|
||||||
- When the order expires.
|
|
||||||
- Timestamp should be formatted as described in RFC3339.
|
|
||||||
- Only required to be included in result when I(status) is C(pending) or C(valid).
|
|
||||||
type: str
|
|
||||||
returned: when server gives expiry date
|
|
||||||
identifiers:
|
|
||||||
description:
|
|
||||||
- List of identifiers this order is for.
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
contains:
|
|
||||||
type:
|
|
||||||
description: Type of identifier. C(dns) or C(ip).
|
|
||||||
type: str
|
|
||||||
value:
|
|
||||||
description: Name of identifier. Hostname or IP address.
|
|
||||||
type: str
|
|
||||||
wildcard:
|
|
||||||
description: "Whether I(value) is actually a wildcard. The wildcard
|
|
||||||
prefix C(*.) is not included in I(value) if this is C(true)."
|
|
||||||
type: bool
|
|
||||||
returned: required to be included if the identifier is wildcarded
|
|
||||||
notBefore:
|
|
||||||
description:
|
|
||||||
- The requested value of the C(notBefore) field in the certificate.
|
|
||||||
- Date should be formatted as described in RFC3339.
|
|
||||||
- Server is not required to return this.
|
|
||||||
type: str
|
|
||||||
returned: when server returns this
|
|
||||||
notAfter:
|
|
||||||
description:
|
|
||||||
- The requested value of the C(notAfter) field in the certificate.
|
|
||||||
- Date should be formatted as described in RFC3339.
|
|
||||||
- Server is not required to return this.
|
|
||||||
type: str
|
|
||||||
returned: when server returns this
|
|
||||||
error:
|
|
||||||
description:
|
|
||||||
- In case an error occurred during processing, this contains information about the error.
|
|
||||||
- The field is structured as a problem document (RFC7807).
|
|
||||||
type: dict
|
|
||||||
returned: when an error occurred
|
|
||||||
authorizations:
|
|
||||||
description:
|
|
||||||
- A list of URLs for authorizations for this order.
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
finalize:
|
|
||||||
description:
|
|
||||||
- A URL used for finalizing an ACME order.
|
|
||||||
type: str
|
|
||||||
certificate:
|
|
||||||
description:
|
|
||||||
- The URL for retrieving the certificate.
|
|
||||||
type: str
|
|
||||||
returned: when certificate was issued
|
|
||||||
'''
|
|
||||||
|
|
||||||
from ansible.module_utils.acme import (
|
|
||||||
ModuleFailException,
|
|
||||||
ACMEAccount,
|
|
||||||
handle_standard_module_arguments,
|
|
||||||
process_links,
|
|
||||||
get_default_argspec,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
|
|
||||||
|
|
||||||
def get_orders_list(module, account, orders_url):
|
|
||||||
'''
|
|
||||||
Retrieves orders list (handles pagination).
|
|
||||||
'''
|
|
||||||
orders = []
|
|
||||||
while orders_url:
|
|
||||||
# Get part of orders list
|
|
||||||
res, info = account.get_request(orders_url, parse_json_result=True, fail_on_error=True)
|
|
||||||
if not res.get('orders'):
|
|
||||||
if orders:
|
|
||||||
module.warn('When retrieving orders list part {0}, got empty result list'.format(orders_url))
|
|
||||||
break
|
|
||||||
# Add order URLs to result list
|
|
||||||
orders.extend(res['orders'])
|
|
||||||
# Extract URL of next part of results list
|
|
||||||
new_orders_url = []
|
|
||||||
|
|
||||||
def f(link, relation):
|
|
||||||
if relation == 'next':
|
|
||||||
new_orders_url.append(link)
|
|
||||||
|
|
||||||
process_links(info, f)
|
|
||||||
new_orders_url.append(None)
|
|
||||||
previous_orders_url, orders_url = orders_url, new_orders_url.pop(0)
|
|
||||||
if orders_url == previous_orders_url:
|
|
||||||
# Prevent infinite loop
|
|
||||||
orders_url = None
|
|
||||||
return orders
|
|
||||||
|
|
||||||
|
|
||||||
def get_order(account, order_url):
|
|
||||||
'''
|
|
||||||
Retrieve order data.
|
|
||||||
'''
|
|
||||||
return account.get_request(order_url, parse_json_result=True, fail_on_error=True)[0]
|
|
||||||
|
|
||||||
|
|
||||||
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 == 'acme_account_facts':
|
|
||||||
module.deprecate("The 'acme_account_facts' module has been renamed to 'acme_account_info'", version='2.12')
|
|
||||||
handle_standard_module_arguments(module, needs_acme_v2=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
account = ACMEAccount(module)
|
|
||||||
# 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': account.uri is not None,
|
|
||||||
'account_uri': account.uri,
|
|
||||||
}
|
|
||||||
if 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'] = 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, account, account_data['orders'])
|
|
||||||
if module.params['retrieve_orders'] == 'url_list':
|
|
||||||
result['orders'] = orders
|
|
||||||
else:
|
|
||||||
result['orders'] = [get_order(account, order) for order in orders]
|
|
||||||
module.exit_json(**result)
|
|
||||||
except ModuleFailException as e:
|
|
||||||
e.do_fail(module)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
File diff suppressed because it is too large
Load Diff
@ -1,223 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: acme_certificate_revoke
|
|
||||||
author: "Felix Fontein (@felixfontein)"
|
|
||||||
version_added: "2.7"
|
|
||||||
short_description: Revoke certificates with the ACME protocol
|
|
||||||
description:
|
|
||||||
- "Allows to revoke certificates issued by a CA supporting the
|
|
||||||
L(ACME protocol,https://tools.ietf.org/html/rfc8555),
|
|
||||||
such as L(Let's Encrypt,https://letsencrypt.org/)."
|
|
||||||
notes:
|
|
||||||
- "Exactly one of C(account_key_src), C(account_key_content),
|
|
||||||
C(private_key_src) or C(private_key_content) must be specified."
|
|
||||||
- "Trying to revoke an already revoked certificate
|
|
||||||
should result in an unchanged status, even if the revocation reason
|
|
||||||
was different than the one specified here. Also, depending on the
|
|
||||||
server, it can happen that some other error is returned if the
|
|
||||||
certificate has already been revoked."
|
|
||||||
seealso:
|
|
||||||
- name: The Let's Encrypt documentation
|
|
||||||
description: Documentation for the Let's Encrypt Certification Authority.
|
|
||||||
Provides useful information for example on rate limits.
|
|
||||||
link: https://letsencrypt.org/docs/
|
|
||||||
- name: Automatic Certificate Management Environment (ACME)
|
|
||||||
description: The specification of the ACME protocol (RFC 8555).
|
|
||||||
link: https://tools.ietf.org/html/rfc8555
|
|
||||||
- module: acme_inspect
|
|
||||||
description: Allows to debug problems.
|
|
||||||
extends_documentation_fragment:
|
|
||||||
- acme
|
|
||||||
options:
|
|
||||||
certificate:
|
|
||||||
description:
|
|
||||||
- "Path to the certificate to revoke."
|
|
||||||
type: path
|
|
||||||
required: yes
|
|
||||||
account_key_src:
|
|
||||||
description:
|
|
||||||
- "Path to a file containing the ACME account RSA or Elliptic Curve
|
|
||||||
key."
|
|
||||||
- "RSA keys can be created with C(openssl rsa ...). Elliptic curve keys can
|
|
||||||
be created with C(openssl ecparam -genkey ...). Any other tool creating
|
|
||||||
private keys in PEM format can be used as well."
|
|
||||||
- "Mutually exclusive with C(account_key_content)."
|
|
||||||
- "Required if C(account_key_content) is not used."
|
|
||||||
type: path
|
|
||||||
account_key_content:
|
|
||||||
description:
|
|
||||||
- "Content of the ACME account RSA or Elliptic Curve key."
|
|
||||||
- "Note that exactly one of C(account_key_src), C(account_key_content),
|
|
||||||
C(private_key_src) or C(private_key_content) must be specified."
|
|
||||||
- "I(Warning): the content will be written into a temporary file, which will
|
|
||||||
be deleted by Ansible when the module completes. Since this is an
|
|
||||||
important private key — it can be used to change the account key,
|
|
||||||
or to revoke your certificates without knowing their private keys
|
|
||||||
—, this might not be acceptable."
|
|
||||||
- "In case C(cryptography) is used, the content is not written into a
|
|
||||||
temporary file. It can still happen that it is written to disk by
|
|
||||||
Ansible in the process of moving the module with its argument to
|
|
||||||
the node where it is executed."
|
|
||||||
type: str
|
|
||||||
private_key_src:
|
|
||||||
description:
|
|
||||||
- "Path to the certificate's private key."
|
|
||||||
- "Note that exactly one of C(account_key_src), C(account_key_content),
|
|
||||||
C(private_key_src) or C(private_key_content) must be specified."
|
|
||||||
type: path
|
|
||||||
private_key_content:
|
|
||||||
description:
|
|
||||||
- "Content of the certificate's private key."
|
|
||||||
- "Note that exactly one of C(account_key_src), C(account_key_content),
|
|
||||||
C(private_key_src) or C(private_key_content) must be specified."
|
|
||||||
- "I(Warning): the content will be written into a temporary file, which will
|
|
||||||
be deleted by Ansible when the module completes. Since this is an
|
|
||||||
important private key — it can be used to change the account key,
|
|
||||||
or to revoke your certificates without knowing their private keys
|
|
||||||
—, this might not be acceptable."
|
|
||||||
- "In case C(cryptography) is used, the content is not written into a
|
|
||||||
temporary file. It can still happen that it is written to disk by
|
|
||||||
Ansible in the process of moving the module with its argument to
|
|
||||||
the node where it is executed."
|
|
||||||
type: str
|
|
||||||
revoke_reason:
|
|
||||||
description:
|
|
||||||
- "One of the revocation reasonCodes defined in
|
|
||||||
L(Section 5.3.1 of RFC5280,https://tools.ietf.org/html/rfc5280#section-5.3.1)."
|
|
||||||
- "Possible values are C(0) (unspecified), C(1) (keyCompromise),
|
|
||||||
C(2) (cACompromise), C(3) (affiliationChanged), C(4) (superseded),
|
|
||||||
C(5) (cessationOfOperation), C(6) (certificateHold),
|
|
||||||
C(8) (removeFromCRL), C(9) (privilegeWithdrawn),
|
|
||||||
C(10) (aACompromise)"
|
|
||||||
type: int
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = '''
|
|
||||||
- name: Revoke certificate with account key
|
|
||||||
acme_certificate_revoke:
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
certificate: /etc/httpd/ssl/sample.com.crt
|
|
||||||
|
|
||||||
- name: Revoke certificate with certificate's private key
|
|
||||||
acme_certificate_revoke:
|
|
||||||
private_key_src: /etc/httpd/ssl/sample.com.key
|
|
||||||
certificate: /etc/httpd/ssl/sample.com.crt
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
'''
|
|
||||||
|
|
||||||
from ansible.module_utils.acme import (
|
|
||||||
ModuleFailException,
|
|
||||||
ACMEAccount,
|
|
||||||
nopad_b64,
|
|
||||||
pem_to_der,
|
|
||||||
handle_standard_module_arguments,
|
|
||||||
get_default_argspec,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
|
|
||||||
|
|
||||||
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),
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
handle_standard_module_arguments(module)
|
|
||||||
|
|
||||||
try:
|
|
||||||
account = ACMEAccount(module)
|
|
||||||
# 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 = account.directory['revoke-cert']
|
|
||||||
payload['resource'] = 'revoke-cert'
|
|
||||||
else:
|
|
||||||
endpoint = account.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:
|
|
||||||
# Step 1: load and parse private key
|
|
||||||
error, private_key_data = account.parse_key(private_key, private_key_content)
|
|
||||||
if error:
|
|
||||||
raise ModuleFailException("error while parsing private key: %s" % error)
|
|
||||||
# Step 2: sign revokation request with private key
|
|
||||||
jws_header = {
|
|
||||||
"alg": private_key_data['alg'],
|
|
||||||
"jwk": private_key_data['jwk'],
|
|
||||||
}
|
|
||||||
result, info = account.send_signed_request(endpoint, payload, key_data=private_key_data, jws_header=jws_header)
|
|
||||||
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 = account.send_signed_request(endpoint, payload)
|
|
||||||
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 ModuleFailException('Error revoking certificate: {0} {1}'.format(info['status'], result))
|
|
||||||
module.exit_json(changed=True)
|
|
||||||
except ModuleFailException as e:
|
|
||||||
e.do_fail(module)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,299 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2018 Felix Fontein <felix@fontein.de>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: acme_challenge_cert_helper
|
|
||||||
author: "Felix Fontein (@felixfontein)"
|
|
||||||
version_added: "2.7"
|
|
||||||
short_description: Prepare certificates required for ACME challenges such as C(tls-alpn-01)
|
|
||||||
description:
|
|
||||||
- "Prepares certificates for ACME challenges such as C(tls-alpn-01)."
|
|
||||||
- "The raw data is provided by the M(acme_certificate) module, and needs to be
|
|
||||||
converted to a certificate to be used for challenge validation. This module
|
|
||||||
provides a simple way to generate the required certificates."
|
|
||||||
seealso:
|
|
||||||
- name: Automatic Certificate Management Environment (ACME)
|
|
||||||
description: The specification of the ACME protocol (RFC 8555).
|
|
||||||
link: https://tools.ietf.org/html/rfc8555
|
|
||||||
- name: ACME TLS ALPN Challenge Extension
|
|
||||||
description: The specification of the C(tls-alpn-01) challenge (RFC 8737).
|
|
||||||
link: https://www.rfc-editor.org/rfc/rfc8737.html
|
|
||||||
requirements:
|
|
||||||
- "cryptography >= 1.3"
|
|
||||||
options:
|
|
||||||
challenge:
|
|
||||||
description:
|
|
||||||
- "The challenge type."
|
|
||||||
type: str
|
|
||||||
required: yes
|
|
||||||
choices:
|
|
||||||
- tls-alpn-01
|
|
||||||
challenge_data:
|
|
||||||
description:
|
|
||||||
- "The C(challenge_data) entry provided by M(acme_certificate) for the challenge."
|
|
||||||
type: dict
|
|
||||||
required: yes
|
|
||||||
private_key_src:
|
|
||||||
description:
|
|
||||||
- "Path to a file containing the private key file to use for this challenge
|
|
||||||
certificate."
|
|
||||||
- "Mutually exclusive with C(private_key_content)."
|
|
||||||
type: path
|
|
||||||
private_key_content:
|
|
||||||
description:
|
|
||||||
- "Content of the private key to use for this challenge certificate."
|
|
||||||
- "Mutually exclusive with C(private_key_src)."
|
|
||||||
type: str
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = '''
|
|
||||||
- name: Create challenges for a given CRT for sample.com
|
|
||||||
acme_certificate:
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
challenge: tls-alpn-01
|
|
||||||
csr: /etc/pki/cert/csr/sample.com.csr
|
|
||||||
dest: /etc/httpd/ssl/sample.com.crt
|
|
||||||
register: sample_com_challenge
|
|
||||||
|
|
||||||
- name: Create certificates for challenges
|
|
||||||
acme_challenge_cert_helper:
|
|
||||||
challenge: tls-alpn-01
|
|
||||||
challenge_data: "{{ item.value['tls-alpn-01'] }}"
|
|
||||||
private_key_src: /etc/pki/cert/key/sample.com.key
|
|
||||||
loop: "{{ sample_com_challenge.challenge_data | dictsort }}"
|
|
||||||
register: sample_com_challenge_certs
|
|
||||||
|
|
||||||
- name: Install challenge certificates
|
|
||||||
# We need to set up HTTPS such that for the domain,
|
|
||||||
# regular_certificate is delivered for regular connections,
|
|
||||||
# except if ALPN selects the "acme-tls/1"; then, the
|
|
||||||
# challenge_certificate must be delivered.
|
|
||||||
# This can for example be achieved with very new versions
|
|
||||||
# of NGINX; search for ssl_preread and
|
|
||||||
# ssl_preread_alpn_protocols for information on how to
|
|
||||||
# route by ALPN protocol.
|
|
||||||
...:
|
|
||||||
domain: "{{ item.domain }}"
|
|
||||||
challenge_certificate: "{{ item.challenge_certificate }}"
|
|
||||||
regular_certificate: "{{ item.regular_certificate }}"
|
|
||||||
private_key: /etc/pki/cert/key/sample.com.key
|
|
||||||
loop: "{{ sample_com_challenge_certs.results }}"
|
|
||||||
|
|
||||||
- name: Create certificate for a given CSR for sample.com
|
|
||||||
acme_certificate:
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
challenge: tls-alpn-01
|
|
||||||
csr: /etc/pki/cert/csr/sample.com.csr
|
|
||||||
dest: /etc/httpd/ssl/sample.com.crt
|
|
||||||
data: "{{ sample_com_challenge }}"
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
domain:
|
|
||||||
description:
|
|
||||||
- "The domain the challenge is for. The certificate should be provided if
|
|
||||||
this is specified in the request's the C(Host) header."
|
|
||||||
returned: always
|
|
||||||
type: str
|
|
||||||
identifier_type:
|
|
||||||
description:
|
|
||||||
- "The identifier type for the actual resource identifier. Will be C(dns)
|
|
||||||
or C(ip)."
|
|
||||||
returned: always
|
|
||||||
type: str
|
|
||||||
version_added: "2.8"
|
|
||||||
identifier:
|
|
||||||
description:
|
|
||||||
- "The identifier for the actual resource. Will be a domain name if the
|
|
||||||
type is C(dns), or an IP address if the type is C(ip)."
|
|
||||||
returned: always
|
|
||||||
type: str
|
|
||||||
version_added: "2.8"
|
|
||||||
challenge_certificate:
|
|
||||||
description:
|
|
||||||
- "The challenge certificate in PEM format."
|
|
||||||
returned: always
|
|
||||||
type: str
|
|
||||||
regular_certificate:
|
|
||||||
description:
|
|
||||||
- "A self-signed certificate for the challenge domain."
|
|
||||||
- "If no existing certificate exists, can be used to set-up
|
|
||||||
https in the first place if that is needed for providing
|
|
||||||
the challenge."
|
|
||||||
returned: always
|
|
||||||
type: str
|
|
||||||
'''
|
|
||||||
|
|
||||||
from ansible.module_utils.acme import (
|
|
||||||
ModuleFailException,
|
|
||||||
read_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
from ansible.module_utils._text import to_bytes, to_text
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import datetime
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
import cryptography.hazmat.backends
|
|
||||||
import cryptography.hazmat.primitives.serialization
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.ec
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.padding
|
|
||||||
import cryptography.hazmat.primitives.hashes
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.utils
|
|
||||||
import cryptography.x509
|
|
||||||
import cryptography.x509.oid
|
|
||||||
import ipaddress
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.3'))
|
|
||||||
_cryptography_backend = cryptography.hazmat.backends.default_backend()
|
|
||||||
except ImportError as dummy:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
HAS_CRYPTOGRAPHY = False
|
|
||||||
|
|
||||||
|
|
||||||
# Convert byte string to ASN1 encoded octet string
|
|
||||||
if sys.version_info[0] >= 3:
|
|
||||||
def encode_octet_string(octet_string):
|
|
||||||
if len(octet_string) >= 128:
|
|
||||||
raise ModuleFailException('Cannot handle octet strings with more than 128 bytes')
|
|
||||||
return bytes([0x4, len(octet_string)]) + octet_string
|
|
||||||
else:
|
|
||||||
def encode_octet_string(octet_string):
|
|
||||||
if len(octet_string) >= 128:
|
|
||||||
raise ModuleFailException('Cannot handle octet strings with more than 128 bytes')
|
|
||||||
return b'\x04' + chr(len(octet_string)) + octet_string
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
challenge=dict(type='str', required=True, choices=['tls-alpn-01']),
|
|
||||||
challenge_data=dict(type='dict', required=True),
|
|
||||||
private_key_src=dict(type='path'),
|
|
||||||
private_key_content=dict(type='str', no_log=True),
|
|
||||||
),
|
|
||||||
required_one_of=(
|
|
||||||
['private_key_src', 'private_key_content'],
|
|
||||||
),
|
|
||||||
mutually_exclusive=(
|
|
||||||
['private_key_src', 'private_key_content'],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if not HAS_CRYPTOGRAPHY:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= 1.3'), exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get parameters
|
|
||||||
challenge = module.params['challenge']
|
|
||||||
challenge_data = module.params['challenge_data']
|
|
||||||
|
|
||||||
# Get hold of private key
|
|
||||||
private_key_content = module.params.get('private_key_content')
|
|
||||||
if private_key_content is None:
|
|
||||||
private_key_content = read_file(module.params['private_key_src'])
|
|
||||||
else:
|
|
||||||
private_key_content = to_bytes(private_key_content)
|
|
||||||
try:
|
|
||||||
private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key(private_key_content, password=None, backend=_cryptography_backend)
|
|
||||||
except Exception as e:
|
|
||||||
raise ModuleFailException('Error while loading private key: {0}'.format(e))
|
|
||||||
|
|
||||||
# Some common attributes
|
|
||||||
domain = to_text(challenge_data['resource'])
|
|
||||||
identifier_type, identifier = to_text(challenge_data.get('resource_original', 'dns:' + challenge_data['resource'])).split(':', 1)
|
|
||||||
subject = issuer = cryptography.x509.Name([])
|
|
||||||
not_valid_before = datetime.datetime.utcnow()
|
|
||||||
not_valid_after = datetime.datetime.utcnow() + datetime.timedelta(days=10)
|
|
||||||
if identifier_type == 'dns':
|
|
||||||
san = cryptography.x509.DNSName(identifier)
|
|
||||||
elif identifier_type == 'ip':
|
|
||||||
san = cryptography.x509.IPAddress(ipaddress.ip_address(identifier))
|
|
||||||
else:
|
|
||||||
raise ModuleFailException('Unsupported identifier type "{0}"'.format(identifier_type))
|
|
||||||
|
|
||||||
# Generate regular self-signed certificate
|
|
||||||
regular_certificate = cryptography.x509.CertificateBuilder().subject_name(
|
|
||||||
subject
|
|
||||||
).issuer_name(
|
|
||||||
issuer
|
|
||||||
).public_key(
|
|
||||||
private_key.public_key()
|
|
||||||
).serial_number(
|
|
||||||
cryptography.x509.random_serial_number()
|
|
||||||
).not_valid_before(
|
|
||||||
not_valid_before
|
|
||||||
).not_valid_after(
|
|
||||||
not_valid_after
|
|
||||||
).add_extension(
|
|
||||||
cryptography.x509.SubjectAlternativeName([san]),
|
|
||||||
critical=False,
|
|
||||||
).sign(
|
|
||||||
private_key,
|
|
||||||
cryptography.hazmat.primitives.hashes.SHA256(),
|
|
||||||
_cryptography_backend
|
|
||||||
)
|
|
||||||
|
|
||||||
# Process challenge
|
|
||||||
if challenge == 'tls-alpn-01':
|
|
||||||
value = base64.b64decode(challenge_data['resource_value'])
|
|
||||||
challenge_certificate = cryptography.x509.CertificateBuilder().subject_name(
|
|
||||||
subject
|
|
||||||
).issuer_name(
|
|
||||||
issuer
|
|
||||||
).public_key(
|
|
||||||
private_key.public_key()
|
|
||||||
).serial_number(
|
|
||||||
cryptography.x509.random_serial_number()
|
|
||||||
).not_valid_before(
|
|
||||||
not_valid_before
|
|
||||||
).not_valid_after(
|
|
||||||
not_valid_after
|
|
||||||
).add_extension(
|
|
||||||
cryptography.x509.SubjectAlternativeName([san]),
|
|
||||||
critical=False,
|
|
||||||
).add_extension(
|
|
||||||
cryptography.x509.UnrecognizedExtension(
|
|
||||||
cryptography.x509.ObjectIdentifier("1.3.6.1.5.5.7.1.31"),
|
|
||||||
encode_octet_string(value),
|
|
||||||
),
|
|
||||||
critical=True,
|
|
||||||
).sign(
|
|
||||||
private_key,
|
|
||||||
cryptography.hazmat.primitives.hashes.SHA256(),
|
|
||||||
_cryptography_backend
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exit_json(
|
|
||||||
changed=True,
|
|
||||||
domain=domain,
|
|
||||||
identifier_type=identifier_type,
|
|
||||||
identifier=identifier,
|
|
||||||
challenge_certificate=challenge_certificate.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM),
|
|
||||||
regular_certificate=regular_certificate.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM)
|
|
||||||
)
|
|
||||||
except ModuleFailException as e:
|
|
||||||
e.do_fail(module)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,320 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2018 Felix Fontein (@felixfontein)
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
---
|
|
||||||
module: acme_inspect
|
|
||||||
author: "Felix Fontein (@felixfontein)"
|
|
||||||
version_added: "2.8"
|
|
||||||
short_description: Send direct requests to an ACME server
|
|
||||||
description:
|
|
||||||
- "Allows to send direct requests to an ACME server with the
|
|
||||||
L(ACME protocol,https://tools.ietf.org/html/rfc8555),
|
|
||||||
which is supported by CAs such as L(Let's Encrypt,https://letsencrypt.org/)."
|
|
||||||
- "This module can be used to debug failed certificate request attempts,
|
|
||||||
for example when M(acme_certificate) fails or encounters a problem which
|
|
||||||
you wish to investigate."
|
|
||||||
- "The module can also be used to directly access features of an ACME servers
|
|
||||||
which are not yet supported by the Ansible ACME modules."
|
|
||||||
notes:
|
|
||||||
- "The I(account_uri) option must be specified for properly authenticated
|
|
||||||
ACME v2 requests (except a C(new-account) request)."
|
|
||||||
- "Using the C(ansible) tool, M(acme_inspect) can be used to directly execute
|
|
||||||
ACME requests without the need of writing a playbook. For example, the
|
|
||||||
following command retrieves the ACME account with ID 1 from Let's Encrypt
|
|
||||||
(assuming C(/path/to/key) is the correct private account key):
|
|
||||||
C(ansible localhost -m acme_inspect -a \"account_key_src=/path/to/key
|
|
||||||
acme_directory=https://acme-v02.api.letsencrypt.org/directory acme_version=2
|
|
||||||
account_uri=https://acme-v02.api.letsencrypt.org/acme/acct/1 method=get
|
|
||||||
url=https://acme-v02.api.letsencrypt.org/acme/acct/1\")"
|
|
||||||
seealso:
|
|
||||||
- name: Automatic Certificate Management Environment (ACME)
|
|
||||||
description: The specification of the ACME protocol (RFC 8555).
|
|
||||||
link: https://tools.ietf.org/html/rfc8555
|
|
||||||
- name: ACME TLS ALPN Challenge Extension
|
|
||||||
description: The specification of the C(tls-alpn-01) challenge (RFC 8737).
|
|
||||||
link: https://www.rfc-editor.org/rfc/rfc8737.html
|
|
||||||
extends_documentation_fragment:
|
|
||||||
- acme
|
|
||||||
options:
|
|
||||||
url:
|
|
||||||
description:
|
|
||||||
- "The URL to send the request to."
|
|
||||||
- "Must be specified if I(method) is not C(directory-only)."
|
|
||||||
type: str
|
|
||||||
method:
|
|
||||||
description:
|
|
||||||
- "The method to use to access the given URL on the ACME server."
|
|
||||||
- "The value C(post) executes an authenticated POST request. The content
|
|
||||||
must be specified in the I(content) option."
|
|
||||||
- "The value C(get) executes an authenticated POST-as-GET request for ACME v2,
|
|
||||||
and a regular GET request for ACME v1."
|
|
||||||
- "The value C(directory-only) only retrieves the directory, without doing
|
|
||||||
a request."
|
|
||||||
type: str
|
|
||||||
default: get
|
|
||||||
choices:
|
|
||||||
- get
|
|
||||||
- post
|
|
||||||
- directory-only
|
|
||||||
content:
|
|
||||||
description:
|
|
||||||
- "An encoded JSON object which will be sent as the content if I(method)
|
|
||||||
is C(post)."
|
|
||||||
- "Required when I(method) is C(post), and not allowed otherwise."
|
|
||||||
type: str
|
|
||||||
fail_on_acme_error:
|
|
||||||
description:
|
|
||||||
- "If I(method) is C(post) or C(get), make the module fail in case an ACME
|
|
||||||
error is returned."
|
|
||||||
type: bool
|
|
||||||
default: yes
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Get directory
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
acme_version: 2
|
|
||||||
method: directory-only
|
|
||||||
register: directory
|
|
||||||
|
|
||||||
- name: Create an account
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
acme_version: 2
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
url: "{{ directory.newAccount}}"
|
|
||||||
method: post
|
|
||||||
content: '{"termsOfServiceAgreed":true}'
|
|
||||||
register: account_creation
|
|
||||||
# account_creation.headers.location contains the account URI
|
|
||||||
# if creation was successful
|
|
||||||
|
|
||||||
- name: Get account information
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
acme_version: 2
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ account_creation.headers.location }}"
|
|
||||||
method: get
|
|
||||||
|
|
||||||
- name: Update account contacts
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
acme_version: 2
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ account_creation.headers.location }}"
|
|
||||||
method: post
|
|
||||||
content: '{{ account_info | to_json }}'
|
|
||||||
vars:
|
|
||||||
account_info:
|
|
||||||
# For valid values, see
|
|
||||||
# https://tools.ietf.org/html/rfc8555#section-7.3
|
|
||||||
contact:
|
|
||||||
- mailto:me@example.com
|
|
||||||
|
|
||||||
- name: Create certificate order
|
|
||||||
acme_certificate:
|
|
||||||
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
acme_version: 2
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
csr: /etc/pki/cert/csr/sample.com.csr
|
|
||||||
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
|
|
||||||
challenge: http-01
|
|
||||||
register: certificate_request
|
|
||||||
|
|
||||||
# Assume something went wrong. certificate_request.order_uri contains
|
|
||||||
# the order URI.
|
|
||||||
|
|
||||||
- name: Get order information
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
acme_version: 2
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ certificate_request.order_uri }}"
|
|
||||||
method: get
|
|
||||||
register: order
|
|
||||||
|
|
||||||
- name: Get first authz for order
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
acme_version: 2
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ order.output_json.authorizations[0] }}"
|
|
||||||
method: get
|
|
||||||
register: authz
|
|
||||||
|
|
||||||
- name: Get HTTP-01 challenge for authz
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
acme_version: 2
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ authz.output_json.challenges | selectattr('type', 'equalto', 'http-01') }}"
|
|
||||||
method: get
|
|
||||||
register: http01challenge
|
|
||||||
|
|
||||||
- name: Activate HTTP-01 challenge manually
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
acme_version: 2
|
|
||||||
account_key_src: /etc/pki/cert/private/account.key
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ http01challenge.url }}"
|
|
||||||
method: post
|
|
||||||
content: '{}'
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
directory:
|
|
||||||
description: The ACME directory's content
|
|
||||||
returned: always
|
|
||||||
type: dict
|
|
||||||
sample: |
|
|
||||||
{
|
|
||||||
"a85k3x9f91A4": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",
|
|
||||||
"keyChange": "https://acme-v02.api.letsencrypt.org/acme/key-change",
|
|
||||||
"meta": {
|
|
||||||
"caaIdentities": [
|
|
||||||
"letsencrypt.org"
|
|
||||||
],
|
|
||||||
"termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf",
|
|
||||||
"website": "https://letsencrypt.org"
|
|
||||||
},
|
|
||||||
"newAccount": "https://acme-v02.api.letsencrypt.org/acme/new-acct",
|
|
||||||
"newNonce": "https://acme-v02.api.letsencrypt.org/acme/new-nonce",
|
|
||||||
"newOrder": "https://acme-v02.api.letsencrypt.org/acme/new-order",
|
|
||||||
"revokeCert": "https://acme-v02.api.letsencrypt.org/acme/revoke-cert"
|
|
||||||
}
|
|
||||||
headers:
|
|
||||||
description: The request's HTTP headers (with lowercase keys)
|
|
||||||
returned: always
|
|
||||||
type: dict
|
|
||||||
sample: |
|
|
||||||
{
|
|
||||||
"boulder-requester": "12345",
|
|
||||||
"cache-control": "max-age=0, no-cache, no-store",
|
|
||||||
"connection": "close",
|
|
||||||
"content-length": "904",
|
|
||||||
"content-type": "application/json",
|
|
||||||
"cookies": {},
|
|
||||||
"cookies_string": "",
|
|
||||||
"date": "Wed, 07 Nov 2018 12:34:56 GMT",
|
|
||||||
"expires": "Wed, 07 Nov 2018 12:44:56 GMT",
|
|
||||||
"link": "<https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf>;rel=\"terms-of-service\"",
|
|
||||||
"msg": "OK (904 bytes)",
|
|
||||||
"pragma": "no-cache",
|
|
||||||
"replay-nonce": "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGH",
|
|
||||||
"server": "nginx",
|
|
||||||
"status": 200,
|
|
||||||
"strict-transport-security": "max-age=604800",
|
|
||||||
"url": "https://acme-v02.api.letsencrypt.org/acme/acct/46161",
|
|
||||||
"x-frame-options": "DENY"
|
|
||||||
}
|
|
||||||
output_text:
|
|
||||||
description: The raw text output
|
|
||||||
returned: always
|
|
||||||
type: str
|
|
||||||
sample: "{\\n \\\"id\\\": 12345,\\n \\\"key\\\": {\\n \\\"kty\\\": \\\"RSA\\\",\\n ..."
|
|
||||||
output_json:
|
|
||||||
description: The output parsed as JSON
|
|
||||||
returned: if output can be parsed as JSON
|
|
||||||
type: dict
|
|
||||||
sample:
|
|
||||||
- id: 12345
|
|
||||||
- key:
|
|
||||||
- kty: RSA
|
|
||||||
- ...
|
|
||||||
'''
|
|
||||||
|
|
||||||
from ansible.module_utils.acme import (
|
|
||||||
ModuleFailException,
|
|
||||||
ACMEAccount,
|
|
||||||
handle_standard_module_arguments,
|
|
||||||
get_default_argspec,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
from ansible.module_utils._text import to_native, to_bytes
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
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],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
handle_standard_module_arguments(module)
|
|
||||||
|
|
||||||
result = dict()
|
|
||||||
changed = False
|
|
||||||
try:
|
|
||||||
# Get hold of ACMEAccount object (includes directory)
|
|
||||||
account = ACMEAccount(module)
|
|
||||||
method = module.params['method']
|
|
||||||
result['directory'] = account.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 = account.get_request(url, parse_json_result=False, fail_on_error=False)
|
|
||||||
elif method == 'post':
|
|
||||||
changed = True # only POSTs can change
|
|
||||||
data, info = account.send_signed_request(url, to_bytes(module.params['content']), parse_json_result=False, encode_payload=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'] = json.loads(data)
|
|
||||||
except Exception as dummy:
|
|
||||||
pass
|
|
||||||
# Fail if error was returned
|
|
||||||
if fail_on_acme_error and info['status'] >= 400:
|
|
||||||
raise ModuleFailException("ACME request failed: CODE: {0} RESULT: {1}".format(info['status'], data))
|
|
||||||
# Done!
|
|
||||||
module.exit_json(changed=changed, **result)
|
|
||||||
except ModuleFailException as e:
|
|
||||||
e.do_fail(module, **result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,350 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# (c) 2018, Felix Fontein <felix@fontein.de>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: certificate_complete_chain
|
|
||||||
author: "Felix Fontein (@felixfontein)"
|
|
||||||
version_added: "2.7"
|
|
||||||
short_description: Complete certificate chain given a set of untrusted and root certificates
|
|
||||||
description:
|
|
||||||
- "This module completes a given chain of certificates in PEM format by finding
|
|
||||||
intermediate certificates from a given set of certificates, until it finds a root
|
|
||||||
certificate in another given set of certificates."
|
|
||||||
- "This can for example be used to find the root certificate for a certificate chain
|
|
||||||
returned by M(acme_certificate)."
|
|
||||||
- "Note that this module does I(not) check for validity of the chains. It only
|
|
||||||
checks that issuer and subject match, and that the signature is correct. It
|
|
||||||
ignores validity dates and key usage completely. If you need to verify that a
|
|
||||||
generated chain is valid, please use C(openssl verify ...)."
|
|
||||||
requirements:
|
|
||||||
- "cryptography >= 1.5"
|
|
||||||
options:
|
|
||||||
input_chain:
|
|
||||||
description:
|
|
||||||
- A concatenated set of certificates in PEM format forming a chain.
|
|
||||||
- The module will try to complete this chain.
|
|
||||||
type: str
|
|
||||||
required: yes
|
|
||||||
root_certificates:
|
|
||||||
description:
|
|
||||||
- "A list of filenames or directories."
|
|
||||||
- "A filename is assumed to point to a file containing one or more certificates
|
|
||||||
in PEM format. All certificates in this file will be added to the set of
|
|
||||||
root certificates."
|
|
||||||
- "If a directory name is given, all files in the directory and its
|
|
||||||
subdirectories will be scanned and tried to be parsed as concatenated
|
|
||||||
certificates in PEM format."
|
|
||||||
- "Symbolic links will be followed."
|
|
||||||
type: list
|
|
||||||
elements: path
|
|
||||||
required: yes
|
|
||||||
intermediate_certificates:
|
|
||||||
description:
|
|
||||||
- "A list of filenames or directories."
|
|
||||||
- "A filename is assumed to point to a file containing one or more certificates
|
|
||||||
in PEM format. All certificates in this file will be added to the set of
|
|
||||||
root certificates."
|
|
||||||
- "If a directory name is given, all files in the directory and its
|
|
||||||
subdirectories will be scanned and tried to be parsed as concatenated
|
|
||||||
certificates in PEM format."
|
|
||||||
- "Symbolic links will be followed."
|
|
||||||
type: list
|
|
||||||
elements: path
|
|
||||||
default: []
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
EXAMPLES = '''
|
|
||||||
# Given a leaf certificate for www.ansible.com and one or more intermediate
|
|
||||||
# certificates, finds the associated root certificate.
|
|
||||||
- name: Find root certificate
|
|
||||||
certificate_complete_chain:
|
|
||||||
input_chain: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com-fullchain.pem') }}"
|
|
||||||
root_certificates:
|
|
||||||
- /etc/ca-certificates/
|
|
||||||
register: www_ansible_com
|
|
||||||
- name: Write root certificate to disk
|
|
||||||
copy:
|
|
||||||
dest: /etc/ssl/csr/www.ansible.com-root.pem
|
|
||||||
content: "{{ www_ansible_com.root }}"
|
|
||||||
|
|
||||||
# Given a leaf certificate for www.ansible.com, and a list of intermediate
|
|
||||||
# certificates, finds the associated root certificate.
|
|
||||||
- name: Find root certificate
|
|
||||||
certificate_complete_chain:
|
|
||||||
input_chain: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.pem') }}"
|
|
||||||
intermediate_certificates:
|
|
||||||
- /etc/ssl/csr/www.ansible.com-chain.pem
|
|
||||||
root_certificates:
|
|
||||||
- /etc/ca-certificates/
|
|
||||||
register: www_ansible_com
|
|
||||||
- name: Write complete chain to disk
|
|
||||||
copy:
|
|
||||||
dest: /etc/ssl/csr/www.ansible.com-completechain.pem
|
|
||||||
content: "{{ ''.join(www_ansible_com.complete_chain) }}"
|
|
||||||
- name: Write root chain (intermediates and root) to disk
|
|
||||||
copy:
|
|
||||||
dest: /etc/ssl/csr/www.ansible.com-rootchain.pem
|
|
||||||
content: "{{ ''.join(www_ansible_com.chain) }}"
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
root:
|
|
||||||
description:
|
|
||||||
- "The root certificate in PEM format."
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
chain:
|
|
||||||
description:
|
|
||||||
- "The chain added to the given input chain. Includes the root certificate."
|
|
||||||
- "Returned as a list of PEM certificates."
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
complete_chain:
|
|
||||||
description:
|
|
||||||
- "The completed chain, including leaf, all intermediates, and root."
|
|
||||||
- "Returned as a list of PEM certificates."
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
'''
|
|
||||||
|
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
from ansible.module_utils._text import to_bytes
|
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
import cryptography.hazmat.backends
|
|
||||||
import cryptography.hazmat.primitives.serialization
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.ec
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.padding
|
|
||||||
import cryptography.hazmat.primitives.hashes
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.utils
|
|
||||||
import cryptography.x509
|
|
||||||
import cryptography.x509.oid
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.5'))
|
|
||||||
_cryptography_backend = cryptography.hazmat.backends.default_backend()
|
|
||||||
except ImportError as dummy:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
HAS_CRYPTOGRAPHY = False
|
|
||||||
|
|
||||||
|
|
||||||
class Certificate(object):
|
|
||||||
'''
|
|
||||||
Stores PEM with parsed certificate.
|
|
||||||
'''
|
|
||||||
def __init__(self, pem, cert):
|
|
||||||
if not (pem.endswith('\n') or pem.endswith('\r')):
|
|
||||||
pem = pem + '\n'
|
|
||||||
self.pem = pem
|
|
||||||
self.cert = cert
|
|
||||||
|
|
||||||
|
|
||||||
def is_parent(module, cert, potential_parent):
|
|
||||||
'''
|
|
||||||
Tests whether the given certificate has been issued by the potential parent certificate.
|
|
||||||
'''
|
|
||||||
# Check issuer
|
|
||||||
if cert.cert.issuer != potential_parent.cert.subject:
|
|
||||||
return False
|
|
||||||
# Check signature
|
|
||||||
public_key = potential_parent.cert.public_key()
|
|
||||||
try:
|
|
||||||
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
|
|
||||||
public_key.verify(
|
|
||||||
cert.cert.signature,
|
|
||||||
cert.cert.tbs_certificate_bytes,
|
|
||||||
cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15(),
|
|
||||||
cert.cert.signature_hash_algorithm
|
|
||||||
)
|
|
||||||
elif isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
|
|
||||||
public_key.verify(
|
|
||||||
cert.cert.signature,
|
|
||||||
cert.cert.tbs_certificate_bytes,
|
|
||||||
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cert.cert.signature_hash_algorithm),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Unknown public key type
|
|
||||||
module.warn('Unknown public key type "{0}"'.format(public_key))
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
except cryptography.exceptions.InvalidSignature as dummy:
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
module.fail_json(msg='Unknown error on signature validation: {0}'.format(e))
|
|
||||||
|
|
||||||
|
|
||||||
def parse_PEM_list(module, text, source, fail_on_error=True):
|
|
||||||
'''
|
|
||||||
Parse concatenated PEM certificates. Return list of ``Certificate`` objects.
|
|
||||||
'''
|
|
||||||
result = []
|
|
||||||
lines = text.splitlines(True)
|
|
||||||
current = None
|
|
||||||
for line in lines:
|
|
||||||
if line.strip():
|
|
||||||
if line.startswith('-----BEGIN '):
|
|
||||||
current = [line]
|
|
||||||
elif current is not None:
|
|
||||||
current.append(line)
|
|
||||||
if line.startswith('-----END '):
|
|
||||||
cert_pem = ''.join(current)
|
|
||||||
current = None
|
|
||||||
# Try to load PEM certificate
|
|
||||||
try:
|
|
||||||
cert = cryptography.x509.load_pem_x509_certificate(to_bytes(cert_pem), _cryptography_backend)
|
|
||||||
result.append(Certificate(cert_pem, cert))
|
|
||||||
except Exception as e:
|
|
||||||
msg = 'Cannot parse certificate #{0} from {1}: {2}'.format(len(result) + 1, source, e)
|
|
||||||
if fail_on_error:
|
|
||||||
module.fail_json(msg=msg)
|
|
||||||
else:
|
|
||||||
module.warn(msg)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def load_PEM_list(module, path, fail_on_error=True):
|
|
||||||
'''
|
|
||||||
Load concatenated PEM certificates from file. Return list of ``Certificate`` objects.
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
with open(path, "rb") as f:
|
|
||||||
return parse_PEM_list(module, f.read().decode('utf-8'), source=path, fail_on_error=fail_on_error)
|
|
||||||
except Exception as e:
|
|
||||||
msg = 'Cannot read certificate file {0}: {1}'.format(path, e)
|
|
||||||
if fail_on_error:
|
|
||||||
module.fail_json(msg=msg)
|
|
||||||
else:
|
|
||||||
module.warn(msg)
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateSet(object):
|
|
||||||
'''
|
|
||||||
Stores a set of certificates. Allows to search for parent (issuer of a certificate).
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
self.module = module
|
|
||||||
self.certificates = set()
|
|
||||||
self.certificate_by_issuer = dict()
|
|
||||||
|
|
||||||
def _load_file(self, path):
|
|
||||||
certs = load_PEM_list(self.module, path, fail_on_error=False)
|
|
||||||
for cert in certs:
|
|
||||||
self.certificates.add(cert)
|
|
||||||
self.certificate_by_issuer[cert.cert.subject] = cert
|
|
||||||
|
|
||||||
def load(self, path):
|
|
||||||
'''
|
|
||||||
Load lists of PEM certificates from a file or a directory.
|
|
||||||
'''
|
|
||||||
b_path = to_bytes(path, errors='surrogate_or_strict')
|
|
||||||
if os.path.isdir(b_path):
|
|
||||||
for directory, dummy, files in os.walk(b_path, followlinks=True):
|
|
||||||
for file in files:
|
|
||||||
self._load_file(os.path.join(directory, file))
|
|
||||||
else:
|
|
||||||
self._load_file(b_path)
|
|
||||||
|
|
||||||
def find_parent(self, cert):
|
|
||||||
'''
|
|
||||||
Search for the parent (issuer) of a certificate. Return ``None`` if none was found.
|
|
||||||
'''
|
|
||||||
potential_parent = self.certificate_by_issuer.get(cert.cert.issuer)
|
|
||||||
if potential_parent is not None:
|
|
||||||
if is_parent(self.module, cert, potential_parent):
|
|
||||||
return potential_parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def format_cert(cert):
|
|
||||||
'''
|
|
||||||
Return human readable representation of certificate for error messages.
|
|
||||||
'''
|
|
||||||
return str(cert.cert)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
input_chain=dict(type='str', required=True),
|
|
||||||
root_certificates=dict(type='list', required=True, elements='path'),
|
|
||||||
intermediate_certificates=dict(type='list', default=[], elements='path'),
|
|
||||||
),
|
|
||||||
supports_check_mode=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not HAS_CRYPTOGRAPHY:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= 1.5'), exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
|
|
||||||
# Load chain
|
|
||||||
chain = parse_PEM_list(module, module.params['input_chain'], source='input chain')
|
|
||||||
if len(chain) == 0:
|
|
||||||
module.fail_json(msg='Input chain must contain at least one certificate')
|
|
||||||
|
|
||||||
# Check chain
|
|
||||||
for i, parent in enumerate(chain):
|
|
||||||
if i > 0:
|
|
||||||
if not is_parent(module, chain[i - 1], parent):
|
|
||||||
module.fail_json(msg=('Cannot verify input chain: certificate #{2}: {3} is not issuer ' +
|
|
||||||
'of certificate #{0}: {1}').format(i, format_cert(chain[i - 1]), i + 1, format_cert(parent)))
|
|
||||||
|
|
||||||
# Load intermediate certificates
|
|
||||||
intermediates = CertificateSet(module)
|
|
||||||
for path in module.params['intermediate_certificates']:
|
|
||||||
intermediates.load(path)
|
|
||||||
|
|
||||||
# Load root certificates
|
|
||||||
roots = CertificateSet(module)
|
|
||||||
for path in module.params['root_certificates']:
|
|
||||||
roots.load(path)
|
|
||||||
|
|
||||||
# Try to complete chain
|
|
||||||
current = chain[-1]
|
|
||||||
completed = []
|
|
||||||
while current:
|
|
||||||
root = roots.find_parent(current)
|
|
||||||
if root:
|
|
||||||
completed.append(root)
|
|
||||||
break
|
|
||||||
intermediate = intermediates.find_parent(current)
|
|
||||||
if intermediate:
|
|
||||||
completed.append(intermediate)
|
|
||||||
current = intermediate
|
|
||||||
else:
|
|
||||||
module.fail_json(msg='Cannot complete chain. Stuck at certificate {0}'.format(format_cert(current)))
|
|
||||||
|
|
||||||
# Return results
|
|
||||||
complete_chain = chain + completed
|
|
||||||
module.exit_json(
|
|
||||||
changed=False,
|
|
||||||
root=complete_chain[-1].pem,
|
|
||||||
chain=[cert.pem for cert in completed],
|
|
||||||
complete_chain=[cert.pem for cert in complete_chain],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
@ -1,952 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright (c), Entrust Datacard Corporation, 2019
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: ecs_certificate
|
|
||||||
author:
|
|
||||||
- Chris Trufan (@ctrufan)
|
|
||||||
version_added: '2.9'
|
|
||||||
short_description: Request SSL/TLS certificates with the Entrust Certificate Services (ECS) API
|
|
||||||
description:
|
|
||||||
- Create, reissue, and renew certificates with the Entrust Certificate Services (ECS) API.
|
|
||||||
- Requires credentials for the L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API.
|
|
||||||
- In order to request a certificate, the domain and organization used in the certificate signing request must be already
|
|
||||||
validated in the ECS system. It is I(not) the responsibility of this module to perform those steps.
|
|
||||||
notes:
|
|
||||||
- C(path) must be specified as the output location of the certificate.
|
|
||||||
requirements:
|
|
||||||
- cryptography >= 1.6
|
|
||||||
options:
|
|
||||||
backup:
|
|
||||||
description:
|
|
||||||
- Whether a backup should be made for the certificate in I(path).
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
force:
|
|
||||||
description:
|
|
||||||
- If force is used, a certificate is requested regardless of whether I(path) points to an existing valid certificate.
|
|
||||||
- If C(request_type=renew), a forced renew will fail if the certificate being renewed has been issued within the past 30 days, regardless of the
|
|
||||||
value of I(remaining_days) or the return value of I(cert_days) - the ECS API does not support the "renew" operation for certificates that are not
|
|
||||||
at least 30 days old.
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- The destination path for the generated certificate as a PEM encoded cert.
|
|
||||||
- If the certificate at this location is not an Entrust issued certificate, a new certificate will always be requested even if the current
|
|
||||||
certificate is technically valid.
|
|
||||||
- If there is already an Entrust certificate at this location, whether it is replaced is depends on the I(remaining_days) calculation.
|
|
||||||
- If an existing certificate is being replaced (see I(remaining_days), I(force), and I(tracking_id)), whether a new certificate is requested
|
|
||||||
or the existing certificate is renewed or reissued is based on I(request_type).
|
|
||||||
type: path
|
|
||||||
required: true
|
|
||||||
full_chain_path:
|
|
||||||
description:
|
|
||||||
- The destination path for the full certificate chain of the certificate, intermediates, and roots.
|
|
||||||
type: path
|
|
||||||
csr:
|
|
||||||
description:
|
|
||||||
- Base-64 encoded Certificate Signing Request (CSR). I(csr) is accepted with or without PEM formatting around the Base-64 string.
|
|
||||||
- If no I(csr) is provided when C(request_type=reissue) or C(request_type=renew), the certificate will be generated with the same public key as
|
|
||||||
the certificate being renewed or reissued.
|
|
||||||
- If I(subject_alt_name) is specified, it will override the subject alternate names in the CSR.
|
|
||||||
- If I(eku) is specified, it will override the extended key usage in the CSR.
|
|
||||||
- If I(ou) is specified, it will override the organizational units "ou=" present in the subject distinguished name of the CSR, if any.
|
|
||||||
- The organization "O=" field from the CSR will not be used. It will be replaced in the issued certificate by I(org) if present, and if not present,
|
|
||||||
the organization tied to I(client_id).
|
|
||||||
type: str
|
|
||||||
tracking_id:
|
|
||||||
description:
|
|
||||||
- The tracking ID of the certificate to reissue or renew.
|
|
||||||
- I(tracking_id) is invalid if C(request_type=new) or C(request_type=validate_only).
|
|
||||||
- If there is a certificate present in I(path) and it is an ECS certificate, I(tracking_id) will be ignored.
|
|
||||||
- If there is no certificate present in I(path) or there is but it is from another provider, the certificate represented by I(tracking_id) will
|
|
||||||
be renewed or reissued and saved to I(path).
|
|
||||||
- If there is no certificate present in I(path) and the I(force) and I(remaining_days) parameters do not indicate a new certificate is needed,
|
|
||||||
the certificate referenced by I(tracking_id) certificate will be saved to I(path).
|
|
||||||
- This can be used when a known certificate is not currently present on a server, but you want to renew or reissue it to be managed by an ansible
|
|
||||||
playbook. For example, if you specify C(request_type=renew), I(tracking_id) of an issued certificate, and I(path) to a file that does not exist,
|
|
||||||
the first run of a task will download the certificate specified by I(tracking_id) (assuming it is still valid). Future runs of the task will
|
|
||||||
(if applicable - see I(force) and I(remaining_days)) renew the certificate now present in I(path).
|
|
||||||
type: int
|
|
||||||
remaining_days:
|
|
||||||
description:
|
|
||||||
- The number of days the certificate must have left being valid. If C(cert_days < remaining_days) then a new certificate will be
|
|
||||||
obtained using I(request_type).
|
|
||||||
- If C(request_type=renew), a renewal will fail if the certificate being renewed has been issued within the past 30 days, so do not set a
|
|
||||||
I(remaining_days) value that is within 30 days of the full lifetime of the certificate being acted upon. (e.g. if you are requesting Certificates
|
|
||||||
with a 90 day lifetime, do not set remaining_days to a value C(60) or higher).
|
|
||||||
- The I(force) option may be used to ensure that a new certificate is always obtained.
|
|
||||||
type: int
|
|
||||||
default: 30
|
|
||||||
request_type:
|
|
||||||
description:
|
|
||||||
- The operation performed if I(tracking_id) references a valid certificate to reissue, or there is already a certificate present in I(path) but
|
|
||||||
either I(force) is specified or C(cert_days < remaining_days).
|
|
||||||
- Specifying C(request_type=validate_only) means the request will be validated against the ECS API, but no certificate will be issued.
|
|
||||||
- Specifying C(request_type=new) means a certificate request will always be submitted and a new certificate issued.
|
|
||||||
- Specifying C(request_type=renew) means that an existing certificate (specified by I(tracking_id) if present, otherwise I(path)) will be renewed.
|
|
||||||
If there is no certificate to renew, a new certificate is requested.
|
|
||||||
- Specifying C(request_type=reissue) means that an existing certificate (specified by I(tracking_id) if present, otherwise I(path)) will be
|
|
||||||
reissued.
|
|
||||||
If there is no certificate to reissue, a new certificate is requested.
|
|
||||||
- If a certificate was issued within the past 30 days, the 'renew' operation is not a valid operation and will fail.
|
|
||||||
- Note that C(reissue) is an operation that will result in the revocation of the certificate that is reissued, be cautious with it's use.
|
|
||||||
- I(check_mode) is only supported if C(request_type=new)
|
|
||||||
- For example, setting C(request_type=renew) and C(remaining_days=30) and pointing to the same certificate on multiple playbook runs means that on
|
|
||||||
the first run new certificate will be requested. It will then be left along on future runs until it is within 30 days of expiry, then the
|
|
||||||
ECS "renew" operation will be performed.
|
|
||||||
type: str
|
|
||||||
choices: [ 'new', 'renew', 'reissue', 'validate_only']
|
|
||||||
default: new
|
|
||||||
cert_type:
|
|
||||||
description:
|
|
||||||
- Specify the type of certificate requested.
|
|
||||||
- If a certificate is being reissued or renewed, this parameter is ignored, and the C(cert_type) of the initial certificate is used.
|
|
||||||
type: str
|
|
||||||
choices: [ 'STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', 'PRIVATE_SSL', 'PD_SSL', 'CODE_SIGNING', 'EV_CODE_SIGNING',
|
|
||||||
'CDS_INDIVIDUAL', 'CDS_GROUP', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT' ]
|
|
||||||
subject_alt_name:
|
|
||||||
description:
|
|
||||||
- The subject alternative name identifiers, as an array of values (applies to I(cert_type) with a value of C(STANDARD_SSL), C(ADVANTAGE_SSL),
|
|
||||||
C(UC_SSL), C(EV_SSL), C(WILDCARD_SSL), C(PRIVATE_SSL), and C(PD_SSL)).
|
|
||||||
- If you are requesting a new SSL certificate, and you pass a I(subject_alt_name) parameter, any SAN names in the CSR are ignored.
|
|
||||||
If no subjectAltName parameter is passed, the SAN names in the CSR are used.
|
|
||||||
- See I(request_type) to understand more about SANs during reissues and renewals.
|
|
||||||
- In the case of certificates of type C(STANDARD_SSL) certificates, if the CN of the certificate is <domain>.<tld> only the www.<domain>.<tld> value
|
|
||||||
is accepted. If the CN of the certificate is www.<domain>.<tld> only the <domain>.<tld> value is accepted.
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
eku:
|
|
||||||
description:
|
|
||||||
- If specified, overrides the key usage in the I(csr).
|
|
||||||
type: str
|
|
||||||
choices: [ SERVER_AUTH, CLIENT_AUTH, SERVER_AND_CLIENT_AUTH ]
|
|
||||||
ct_log:
|
|
||||||
description:
|
|
||||||
- In compliance with browser requirements, this certificate may be posted to the Certificate Transparency (CT) logs. This is a best practice
|
|
||||||
technique that helps domain owners monitor certificates issued to their domains. Note that not all certificates are eligible for CT logging.
|
|
||||||
- If I(ct_log) is not specified, the certificate uses the account default.
|
|
||||||
- If I(ct_log) is specified and the account settings allow it, I(ct_log) overrides the account default.
|
|
||||||
- If I(ct_log) is set to C(false), but the account settings are set to "always log", the certificate generation will fail.
|
|
||||||
type: bool
|
|
||||||
client_id:
|
|
||||||
description:
|
|
||||||
- The client ID to submit the Certificate Signing Request under.
|
|
||||||
- If no client ID is specified, the certificate will be submitted under the primary client with ID of 1.
|
|
||||||
- When using a client other than the primary client, the I(org) parameter cannot be specified.
|
|
||||||
- The issued certificate will have an organization value in the subject distinguished name represented by the client.
|
|
||||||
type: int
|
|
||||||
default: 1
|
|
||||||
org:
|
|
||||||
description:
|
|
||||||
- Organization "O=" to include in the certificate.
|
|
||||||
- If I(org) is not specified, the organization from the client represented by I(client_id) is used.
|
|
||||||
- Unless the I(cert_type) is C(PD_SSL), this field may not be specified if the value of I(client_id) is not "1" (the primary client).
|
|
||||||
non-primary clients, certificates may only be issued with the organization of that client.
|
|
||||||
type: str
|
|
||||||
ou:
|
|
||||||
description:
|
|
||||||
- Organizational unit "OU=" to include in the certificate.
|
|
||||||
- I(ou) behavior is dependent on whether organizational units are enabled for your account. If organizational unit support is disabled for your
|
|
||||||
account, organizational units from the I(csr) and the I(ou) parameter are ignored.
|
|
||||||
- If both I(csr) and I(ou) are specified, the value in I(ou) will override the OU fields present in the subject distinguished name in the I(csr)
|
|
||||||
- If neither I(csr) nor I(ou) are specified for a renew or reissue operation, the OU fields in the initial certificate are reused.
|
|
||||||
- An invalid OU from I(csr) is ignored, but any invalid organizational units in I(ou) will result in an error indicating "Unapproved OU". The I(ou)
|
|
||||||
parameter can be used to force failure if an unapproved organizational unit is provided.
|
|
||||||
- A maximum of one OU may be specified for current products. Multiple OUs are reserved for future products.
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
end_user_key_storage_agreement:
|
|
||||||
description:
|
|
||||||
- The end user of the Code Signing certificate must generate and store the private key for this request on cryptographically secure
|
|
||||||
hardware to be compliant with the Entrust CSP and Subscription agreement. If requesting a certificate of type C(CODE_SIGNING) or
|
|
||||||
C(EV_CODE_SIGNING), you must set I(end_user_key_storage_agreement) to true if and only if you acknowledge that you will inform the user of this
|
|
||||||
requirement.
|
|
||||||
- Applicable only to I(cert_type) of values C(CODE_SIGNING) and C(EV_CODE_SIGNING).
|
|
||||||
type: bool
|
|
||||||
tracking_info:
|
|
||||||
description: Free form tracking information to attach to the record for the certificate.
|
|
||||||
type: str
|
|
||||||
requester_name:
|
|
||||||
description: The requester name to associate with certificate tracking information.
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
requester_email:
|
|
||||||
description: The requester email to associate with certificate tracking information and receive delivery and expiry notices for the certificate.
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
requester_phone:
|
|
||||||
description: The requester phone number to associate with certificate tracking information.
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
additional_emails:
|
|
||||||
description: A list of additional email addresses to receive the delivery notice and expiry notification for the certificate.
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
custom_fields:
|
|
||||||
description:
|
|
||||||
- Mapping of custom fields to associate with the certificate request and certificate.
|
|
||||||
- Only supported if custom fields are enabled for your account.
|
|
||||||
- Each custom field specified must be a custom field you have defined for your account.
|
|
||||||
type: dict
|
|
||||||
suboptions:
|
|
||||||
text1:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text2:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text3:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text4:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text5:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text6:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text7:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text8:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text9:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text10:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text11:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text12:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text13:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text14:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
text15:
|
|
||||||
description: Custom text field (maximum 500 characters)
|
|
||||||
type: str
|
|
||||||
number1:
|
|
||||||
description: Custom number field.
|
|
||||||
type: float
|
|
||||||
number2:
|
|
||||||
description: Custom number field.
|
|
||||||
type: float
|
|
||||||
number3:
|
|
||||||
description: Custom number field.
|
|
||||||
type: float
|
|
||||||
number4:
|
|
||||||
description: Custom number field.
|
|
||||||
type: float
|
|
||||||
number5:
|
|
||||||
description: Custom number field.
|
|
||||||
type: float
|
|
||||||
date1:
|
|
||||||
description: Custom date field.
|
|
||||||
type: str
|
|
||||||
date2:
|
|
||||||
description: Custom date field.
|
|
||||||
type: str
|
|
||||||
date3:
|
|
||||||
description: Custom date field.
|
|
||||||
type: str
|
|
||||||
date4:
|
|
||||||
description: Custom date field.
|
|
||||||
type: str
|
|
||||||
date5:
|
|
||||||
description: Custom date field.
|
|
||||||
type: str
|
|
||||||
email1:
|
|
||||||
description: Custom email field.
|
|
||||||
type: str
|
|
||||||
email2:
|
|
||||||
description: Custom email field.
|
|
||||||
type: str
|
|
||||||
email3:
|
|
||||||
description: Custom email field.
|
|
||||||
type: str
|
|
||||||
email4:
|
|
||||||
description: Custom email field.
|
|
||||||
type: str
|
|
||||||
email5:
|
|
||||||
description: Custom email field.
|
|
||||||
type: str
|
|
||||||
dropdown1:
|
|
||||||
description: Custom dropdown field.
|
|
||||||
type: str
|
|
||||||
dropdown2:
|
|
||||||
description: Custom dropdown field.
|
|
||||||
type: str
|
|
||||||
dropdown3:
|
|
||||||
description: Custom dropdown field.
|
|
||||||
type: str
|
|
||||||
dropdown4:
|
|
||||||
description: Custom dropdown field.
|
|
||||||
type: str
|
|
||||||
dropdown5:
|
|
||||||
description: Custom dropdown field.
|
|
||||||
type: str
|
|
||||||
cert_expiry:
|
|
||||||
description:
|
|
||||||
- The date the certificate should be set to expire, in RFC3339 compliant date or date-time format. For example,
|
|
||||||
C(2020-02-23), C(2020-02-23T15:00:00.05Z).
|
|
||||||
- I(cert_expiry) is only supported for requests of C(request_type=new) or C(request_type=renew). If C(request_type=reissue),
|
|
||||||
I(cert_expiry) will be used for the first certificate issuance, but subsequent issuances will have the same expiry as the initial
|
|
||||||
certificate.
|
|
||||||
- A reissued certificate will always have the same expiry as the original certificate.
|
|
||||||
- Note that only the date (day, month, year) is supported for specifying the expiry date. If you choose to specify an expiry time with the expiry
|
|
||||||
date, the time will be adjusted to Eastern Standard Time (EST). This could have the unintended effect of moving your expiry date to the previous
|
|
||||||
day.
|
|
||||||
- Applies only to accounts with a pooling inventory model.
|
|
||||||
- Only one of I(cert_expiry) or I(cert_lifetime) may be specified.
|
|
||||||
type: str
|
|
||||||
cert_lifetime:
|
|
||||||
description:
|
|
||||||
- The lifetime of the certificate.
|
|
||||||
- Applies to all certificates for accounts with a non-pooling inventory model.
|
|
||||||
- I(cert_lifetime) is only supported for requests of C(request_type=new) or C(request_type=renew). If C(request_type=reissue), I(cert_lifetime) will
|
|
||||||
be used for the first certificate issuance, but subsequent issuances will have the same expiry as the initial certificate.
|
|
||||||
- Applies to certificates of I(cert_type)=C(CDS_INDIVIDUAL, CDS_GROUP, CDS_ENT_LITE, CDS_ENT_PRO, SMIME_ENT) for accounts with a pooling inventory
|
|
||||||
model.
|
|
||||||
- C(P1Y) is a certificate with a 1 year lifetime.
|
|
||||||
- C(P2Y) is a certificate with a 2 year lifetime.
|
|
||||||
- C(P3Y) is a certificate with a 3 year lifetime.
|
|
||||||
- Only one of I(cert_expiry) or I(cert_lifetime) may be specified.
|
|
||||||
type: str
|
|
||||||
choices: [ P1Y, P2Y, P3Y ]
|
|
||||||
seealso:
|
|
||||||
- module: openssl_privatekey
|
|
||||||
description: Can be used to create private keys (both for certificates and accounts).
|
|
||||||
- module: openssl_csr
|
|
||||||
description: Can be used to create a Certificate Signing Request (CSR).
|
|
||||||
extends_documentation_fragment:
|
|
||||||
- ecs_credential
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Request a new certificate from Entrust with bare minimum parameters.
|
|
||||||
Will request a new certificate if current one is valid but within 30
|
|
||||||
days of expiry. If replacing an existing file in path, will back it up.
|
|
||||||
ecs_certificate:
|
|
||||||
backup: true
|
|
||||||
path: /etc/ssl/crt/ansible.com.crt
|
|
||||||
full_chain_path: /etc/ssl/crt/ansible.com.chain.crt
|
|
||||||
csr: /etc/ssl/csr/ansible.com.csr
|
|
||||||
cert_type: EV_SSL
|
|
||||||
requester_name: Jo Doe
|
|
||||||
requester_email: jdoe@ansible.com
|
|
||||||
requester_phone: 555-555-5555
|
|
||||||
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-client.key
|
|
||||||
|
|
||||||
- name: If there is no certificate present in path, request a new certificate
|
|
||||||
of type EV_SSL. Otherwise, if there is an Entrust managed certificate
|
|
||||||
in path and it is within 63 days of expiration, request a renew of that
|
|
||||||
certificate.
|
|
||||||
ecs_certificate:
|
|
||||||
path: /etc/ssl/crt/ansible.com.crt
|
|
||||||
csr: /etc/ssl/csr/ansible.com.csr
|
|
||||||
cert_type: EV_SSL
|
|
||||||
cert_expiry: '2020-08-20'
|
|
||||||
request_type: renew
|
|
||||||
remaining_days: 63
|
|
||||||
requester_name: Jo Doe
|
|
||||||
requester_email: jdoe@ansible.com
|
|
||||||
requester_phone: 555-555-5555
|
|
||||||
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-client.key
|
|
||||||
|
|
||||||
- name: If there is no certificate present in path, download certificate
|
|
||||||
specified by tracking_id if it is still valid. Otherwise, if the
|
|
||||||
certificate is within 79 days of expiration, request a renew of that
|
|
||||||
certificate and save it in path. This can be used to "migrate" a
|
|
||||||
certificate to be Ansible managed.
|
|
||||||
ecs_certificate:
|
|
||||||
path: /etc/ssl/crt/ansible.com.crt
|
|
||||||
csr: /etc/ssl/csr/ansible.com.csr
|
|
||||||
tracking_id: 2378915
|
|
||||||
request_type: renew
|
|
||||||
remaining_days: 79
|
|
||||||
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-client.key
|
|
||||||
|
|
||||||
- name: Force a reissue of the certificate specified by tracking_id.
|
|
||||||
ecs_certificate:
|
|
||||||
path: /etc/ssl/crt/ansible.com.crt
|
|
||||||
force: true
|
|
||||||
tracking_id: 2378915
|
|
||||||
request_type: reissue
|
|
||||||
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-client.key
|
|
||||||
|
|
||||||
- name: Request a new certificate with an alternative client. Note that the
|
|
||||||
issued certificate will have it's Subject Distinguished Name use the
|
|
||||||
organization details associated with that client, rather than what is
|
|
||||||
in the CSR.
|
|
||||||
ecs_certificate:
|
|
||||||
path: /etc/ssl/crt/ansible.com.crt
|
|
||||||
csr: /etc/ssl/csr/ansible.com.csr
|
|
||||||
client_id: 2
|
|
||||||
requester_name: Jo Doe
|
|
||||||
requester_email: jdoe@ansible.com
|
|
||||||
requester_phone: 555-555-5555
|
|
||||||
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-client.key
|
|
||||||
|
|
||||||
- name: Request a new certificate with a number of CSR parameters overridden
|
|
||||||
and tracking information
|
|
||||||
ecs_certificate:
|
|
||||||
path: /etc/ssl/crt/ansible.com.crt
|
|
||||||
full_chain_path: /etc/ssl/crt/ansible.com.chain.crt
|
|
||||||
csr: /etc/ssl/csr/ansible.com.csr
|
|
||||||
subject_alt_name:
|
|
||||||
- ansible.testcertificates.com
|
|
||||||
- www.testcertificates.com
|
|
||||||
eku: SERVER_AND_CLIENT_AUTH
|
|
||||||
ct_log: true
|
|
||||||
org: Test Organization Inc.
|
|
||||||
ou:
|
|
||||||
- Administration
|
|
||||||
tracking_info: "Submitted via Ansible"
|
|
||||||
additional_emails:
|
|
||||||
- itsupport@testcertificates.com
|
|
||||||
- jsmith@ansible.com
|
|
||||||
custom_fields:
|
|
||||||
text1: Admin
|
|
||||||
text2: Invoice 25
|
|
||||||
number1: 342
|
|
||||||
date1: '2018-01-01'
|
|
||||||
email1: sales@ansible.testcertificates.com
|
|
||||||
dropdown1: red
|
|
||||||
cert_expiry: '2020-08-15'
|
|
||||||
requester_name: Jo Doe
|
|
||||||
requester_email: jdoe@ansible.com
|
|
||||||
requester_phone: 555-555-5555
|
|
||||||
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-client.key
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
filename:
|
|
||||||
description: The destination path for the generated certificate.
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: /etc/ssl/crt/www.ansible.com.crt
|
|
||||||
backup_file:
|
|
||||||
description: Name of backup file created for the certificate.
|
|
||||||
returned: changed and if I(backup) is C(true)
|
|
||||||
type: str
|
|
||||||
sample: /path/to/www.ansible.com.crt.2019-03-09@11:22~
|
|
||||||
backup_full_chain_file:
|
|
||||||
description: Name of the backup file created for the certificate chain.
|
|
||||||
returned: changed and if I(backup) is C(true) and I(full_chain_path) is set.
|
|
||||||
type: str
|
|
||||||
sample: /path/to/ca.chain.crt.2019-03-09@11:22~
|
|
||||||
tracking_id:
|
|
||||||
description: The tracking ID to reference and track the certificate in ECS.
|
|
||||||
returned: success
|
|
||||||
type: int
|
|
||||||
sample: 380079
|
|
||||||
serial_number:
|
|
||||||
description: The serial number of the issued certificate.
|
|
||||||
returned: success
|
|
||||||
type: int
|
|
||||||
sample: 1235262234164342
|
|
||||||
cert_days:
|
|
||||||
description: The number of days the certificate remains valid.
|
|
||||||
returned: success
|
|
||||||
type: int
|
|
||||||
sample: 253
|
|
||||||
cert_status:
|
|
||||||
description:
|
|
||||||
- The certificate status in ECS.
|
|
||||||
- 'Current possible values (which may be expanded in the future) are: C(ACTIVE), C(APPROVED), C(DEACTIVATED), C(DECLINED), C(EXPIRED), C(NA),
|
|
||||||
C(PENDING), C(PENDING_QUORUM), C(READY), C(REISSUED), C(REISSUING), C(RENEWED), C(RENEWING), C(REVOKED), C(SUSPENDED)'
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: ACTIVE
|
|
||||||
cert_details:
|
|
||||||
description:
|
|
||||||
- The full response JSON from the Get Certificate call of the ECS API.
|
|
||||||
- 'While the response contents are guaranteed to be forwards compatible with new ECS API releases, Entrust recommends that you do not make any
|
|
||||||
playbooks take actions based on the content of this field. However it may be useful for debugging, logging, or auditing purposes.'
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
from ansible.module_utils.ecs.api import (
|
|
||||||
ecs_client_argument_spec,
|
|
||||||
ECSClient,
|
|
||||||
RestOperationException,
|
|
||||||
SessionConfigurationException,
|
|
||||||
)
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
|
|
||||||
from ansible.module_utils import crypto as crypto_utils
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
from ansible.module_utils._text import to_native, to_bytes
|
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
|
||||||
|
|
||||||
|
|
||||||
def validate_cert_expiry(cert_expiry):
|
|
||||||
search_string_partial = re.compile(r'^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])\Z')
|
|
||||||
search_string_full = re.compile(r'^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):'
|
|
||||||
r'([0-5][0-9]|60)(.[0-9]+)?(([Zz])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))\Z')
|
|
||||||
if search_string_partial.match(cert_expiry) or search_string_full.match(cert_expiry):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_cert_days(expires_after):
|
|
||||||
cert_days = 0
|
|
||||||
if expires_after:
|
|
||||||
expires_after_datetime = datetime.datetime.strptime(expires_after, '%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
cert_days = (expires_after_datetime - datetime.datetime.now()).days
|
|
||||||
return cert_days
|
|
||||||
|
|
||||||
|
|
||||||
# Populate the value of body[dict_param_name] with the JSON equivalent of
|
|
||||||
# module parameter of param_name if that parameter is present, otherwise leave field
|
|
||||||
# out of resulting dict
|
|
||||||
def convert_module_param_to_json_bool(module, dict_param_name, param_name):
|
|
||||||
body = {}
|
|
||||||
if module.params[param_name] is not None:
|
|
||||||
if module.params[param_name]:
|
|
||||||
body[dict_param_name] = 'true'
|
|
||||||
else:
|
|
||||||
body[dict_param_name] = 'false'
|
|
||||||
return body
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def custom_fields_spec():
|
|
||||||
return dict(
|
|
||||||
text1=dict(type='str'),
|
|
||||||
text2=dict(type='str'),
|
|
||||||
text3=dict(type='str'),
|
|
||||||
text4=dict(type='str'),
|
|
||||||
text5=dict(type='str'),
|
|
||||||
text6=dict(type='str'),
|
|
||||||
text7=dict(type='str'),
|
|
||||||
text8=dict(type='str'),
|
|
||||||
text9=dict(type='str'),
|
|
||||||
text10=dict(type='str'),
|
|
||||||
text11=dict(type='str'),
|
|
||||||
text12=dict(type='str'),
|
|
||||||
text13=dict(type='str'),
|
|
||||||
text14=dict(type='str'),
|
|
||||||
text15=dict(type='str'),
|
|
||||||
number1=dict(type='float'),
|
|
||||||
number2=dict(type='float'),
|
|
||||||
number3=dict(type='float'),
|
|
||||||
number4=dict(type='float'),
|
|
||||||
number5=dict(type='float'),
|
|
||||||
date1=dict(type='str'),
|
|
||||||
date2=dict(type='str'),
|
|
||||||
date3=dict(type='str'),
|
|
||||||
date4=dict(type='str'),
|
|
||||||
date5=dict(type='str'),
|
|
||||||
email1=dict(type='str'),
|
|
||||||
email2=dict(type='str'),
|
|
||||||
email3=dict(type='str'),
|
|
||||||
email4=dict(type='str'),
|
|
||||||
email5=dict(type='str'),
|
|
||||||
dropdown1=dict(type='str'),
|
|
||||||
dropdown2=dict(type='str'),
|
|
||||||
dropdown3=dict(type='str'),
|
|
||||||
dropdown4=dict(type='str'),
|
|
||||||
dropdown5=dict(type='str'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def ecs_certificate_argument_spec():
|
|
||||||
return dict(
|
|
||||||
backup=dict(type='bool', default=False),
|
|
||||||
force=dict(type='bool', default=False),
|
|
||||||
path=dict(type='path', required=True),
|
|
||||||
full_chain_path=dict(type='path'),
|
|
||||||
tracking_id=dict(type='int'),
|
|
||||||
remaining_days=dict(type='int', default=30),
|
|
||||||
request_type=dict(type='str', default='new', choices=['new', 'renew', 'reissue', 'validate_only']),
|
|
||||||
cert_type=dict(type='str', choices=['STANDARD_SSL',
|
|
||||||
'ADVANTAGE_SSL',
|
|
||||||
'UC_SSL',
|
|
||||||
'EV_SSL',
|
|
||||||
'WILDCARD_SSL',
|
|
||||||
'PRIVATE_SSL',
|
|
||||||
'PD_SSL',
|
|
||||||
'CODE_SIGNING',
|
|
||||||
'EV_CODE_SIGNING',
|
|
||||||
'CDS_INDIVIDUAL',
|
|
||||||
'CDS_GROUP',
|
|
||||||
'CDS_ENT_LITE',
|
|
||||||
'CDS_ENT_PRO',
|
|
||||||
'SMIME_ENT',
|
|
||||||
]),
|
|
||||||
csr=dict(type='str'),
|
|
||||||
subject_alt_name=dict(type='list', elements='str'),
|
|
||||||
eku=dict(type='str', choices=['SERVER_AUTH', 'CLIENT_AUTH', 'SERVER_AND_CLIENT_AUTH']),
|
|
||||||
ct_log=dict(type='bool'),
|
|
||||||
client_id=dict(type='int', default=1),
|
|
||||||
org=dict(type='str'),
|
|
||||||
ou=dict(type='list', elements='str'),
|
|
||||||
end_user_key_storage_agreement=dict(type='bool'),
|
|
||||||
tracking_info=dict(type='str'),
|
|
||||||
requester_name=dict(type='str', required=True),
|
|
||||||
requester_email=dict(type='str', required=True),
|
|
||||||
requester_phone=dict(type='str', required=True),
|
|
||||||
additional_emails=dict(type='list', elements='str'),
|
|
||||||
custom_fields=dict(type='dict', default=None, options=custom_fields_spec()),
|
|
||||||
cert_expiry=dict(type='str'),
|
|
||||||
cert_lifetime=dict(type='str', choices=['P1Y', 'P2Y', 'P3Y']),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
ecs_argument_spec = ecs_client_argument_spec()
|
|
||||||
ecs_argument_spec.update(ecs_certificate_argument_spec())
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=ecs_argument_spec,
|
|
||||||
required_if=(
|
|
||||||
['request_type', 'new', ['cert_type']],
|
|
||||||
['request_type', 'validate_only', ['cert_type']],
|
|
||||||
['cert_type', 'CODE_SIGNING', ['end_user_key_storage_agreement']],
|
|
||||||
['cert_type', 'EV_CODE_SIGNING', ['end_user_key_storage_agreement']],
|
|
||||||
),
|
|
||||||
mutually_exclusive=(
|
|
||||||
['cert_expiry', 'cert_lifetime'],
|
|
||||||
),
|
|
||||||
supports_check_mode=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not CRYPTOGRAPHY_FOUND or CRYPTOGRAPHY_VERSION < LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION):
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
|
|
||||||
# If validate_only is used, pointing to an existing tracking_id is an invalid operation
|
|
||||||
if module.params['tracking_id']:
|
|
||||||
if module.params['request_type'] == 'new' or module.params['request_type'] == 'validate_only':
|
|
||||||
module.fail_json(msg='The tracking_id field is invalid when request_type="{0}".'.format(module.params['request_type']))
|
|
||||||
|
|
||||||
# A reissued request can not specify an expiration date or lifetime
|
|
||||||
if module.params['request_type'] == 'reissue':
|
|
||||||
if module.params['cert_expiry']:
|
|
||||||
module.fail_json(msg='The cert_expiry field is invalid when request_type="reissue".')
|
|
||||||
elif module.params['cert_lifetime']:
|
|
||||||
module.fail_json(msg='The cert_lifetime field is invalid when request_type="reissue".')
|
|
||||||
# Only a reissued request can omit the CSR
|
|
||||||
else:
|
|
||||||
module_params_csr = module.params['csr']
|
|
||||||
if module_params_csr is None:
|
|
||||||
module.fail_json(msg='The csr field is required when request_type={0}'.format(module.params['request_type']))
|
|
||||||
elif not os.path.exists(module_params_csr):
|
|
||||||
module.fail_json(msg='The csr field of {0} was not a valid path. csr is required when request_type={1}'.format(
|
|
||||||
module_params_csr, module.params['request_type']))
|
|
||||||
|
|
||||||
if module.params['ou'] and len(module.params['ou']) > 1:
|
|
||||||
module.fail_json(msg='Multiple "ou" values are not currently supported.')
|
|
||||||
|
|
||||||
if module.params['end_user_key_storage_agreement']:
|
|
||||||
if module.params['cert_type'] != 'CODE_SIGNING' and module.params['cert_type'] != 'EV_CODE_SIGNING':
|
|
||||||
module.fail_json(msg='Parameter "end_user_key_storage_agreement" is valid only for cert_types "CODE_SIGNING" and "EV_CODE_SIGNING"')
|
|
||||||
|
|
||||||
if module.params['org'] and module.params['client_id'] != 1 and module.params['cert_type'] != 'PD_SSL':
|
|
||||||
module.fail_json(msg='The "org" parameter is not supported when client_id parameter is set to a value other than 1, unless cert_type is "PD_SSL".')
|
|
||||||
|
|
||||||
if module.params['cert_expiry']:
|
|
||||||
if not validate_cert_expiry(module.params['cert_expiry']):
|
|
||||||
module.fail_json(msg='The "cert_expiry" parameter of "{0}" is not a valid date or date-time'.format(module.params['cert_expiry']))
|
|
||||||
|
|
||||||
certificate = EcsCertificate(module)
|
|
||||||
certificate.request_cert(module)
|
|
||||||
result = certificate.dump()
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,409 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright 2019 Entrust Datacard Corporation.
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: ecs_domain
|
|
||||||
author:
|
|
||||||
- Chris Trufan (@ctrufan)
|
|
||||||
version_added: '2.10'
|
|
||||||
short_description: Request validation of a domain with the Entrust Certificate Services (ECS) API
|
|
||||||
description:
|
|
||||||
- Request validation or re-validation of a domain with the Entrust Certificate Services (ECS) API.
|
|
||||||
- Requires credentials for the L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API.
|
|
||||||
- If the domain is already in the validation process, no new validation will be requested, but the validation data (if applicable) will be returned.
|
|
||||||
- If the domain is already in the validation process but the I(verification_method) specified is different than the current I(verification_method),
|
|
||||||
the I(verification_method) will be updated and validation data (if applicable) will be returned.
|
|
||||||
- If the domain is an active, validated domain, the return value of I(changed) will be false, unless C(domain_status=EXPIRED), in which case a re-validation
|
|
||||||
will be performed.
|
|
||||||
- If C(verification_method=dns), details about the required DNS entry will be specified in the return parameters I(dns_contents), I(dns_location), and
|
|
||||||
I(dns_resource_type).
|
|
||||||
- If C(verification_method=web_server), details about the required file details will be specified in the return parameters I(file_contents) and
|
|
||||||
I(file_location).
|
|
||||||
- If C(verification_method=email), the email address(es) that the validation email(s) were sent to will be in the return parameter I(emails). This is
|
|
||||||
purely informational. For domains requested using this module, this will always be a list of size 1.
|
|
||||||
notes:
|
|
||||||
- There is a small delay (typically about 5 seconds, but can be as long as 60 seconds) before obtaining the random values when requesting a validation
|
|
||||||
while C(verification_method=dns) or C(verification_method=web_server). Be aware of that if doing many domain validation requests.
|
|
||||||
options:
|
|
||||||
client_id:
|
|
||||||
description:
|
|
||||||
- The client ID to request the domain be associated with.
|
|
||||||
- If no client ID is specified, the domain will be added under the primary client with ID of 1.
|
|
||||||
type: int
|
|
||||||
default: 1
|
|
||||||
domain_name:
|
|
||||||
description:
|
|
||||||
- The domain name to be verified or reverified.
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
verification_method:
|
|
||||||
description:
|
|
||||||
- The verification method to be used to prove control of the domain.
|
|
||||||
- If C(verification_method=email) and the value I(verification_email) is specified, that value is used for the email validation. If
|
|
||||||
I(verification_email) is not provided, the first value present in WHOIS data will be used. An email will be sent to the address in
|
|
||||||
I(verification_email) with instructions on how to verify control of the domain.
|
|
||||||
- If C(verification_method=dns), the value I(dns_contents) must be stored in location I(dns_location), with a DNS record type of
|
|
||||||
I(verification_dns_record_type). To prove domain ownership, update your DNS records so the text string returned by I(dns_contents) is available at
|
|
||||||
I(dns_location).
|
|
||||||
- If C(verification_method=web_server), the contents of return value I(file_contents) must be made available on a web server accessible at location
|
|
||||||
I(file_location).
|
|
||||||
- If C(verification_method=manual), the domain will be validated with a manual process. This is not recommended.
|
|
||||||
type: str
|
|
||||||
choices: [ 'dns', 'email', 'manual', 'web_server']
|
|
||||||
required: true
|
|
||||||
verification_email:
|
|
||||||
description:
|
|
||||||
- Email address to be used to verify domain ownership.
|
|
||||||
- 'Email address must be either an email address present in the WHOIS data for I(domain_name), or one of the following constructed emails:
|
|
||||||
admin@I(domain_name), administrator@I(domain_name), webmaster@I(domain_name), hostmaster@I(domain_name), postmaster@I(domain_name)'
|
|
||||||
- 'Note that if I(domain_name) includes subdomains, the top level domain should be used. For example, if requesting validation of
|
|
||||||
example1.ansible.com, or test.example2.ansible.com, and you want to use the "admin" preconstructed name, the email address should be
|
|
||||||
admin@ansible.com.'
|
|
||||||
- If using the email values from the WHOIS data for the domain or it's top level namespace, they must be exact matches.
|
|
||||||
- If C(verification_method=email) but I(verification_email) is not provided, the first email address found in WHOIS data for the domain will be
|
|
||||||
used.
|
|
||||||
- To verify domain ownership, domain owner must follow the instructions in the email they receive.
|
|
||||||
- Only allowed if C(verification_method=email)
|
|
||||||
type: str
|
|
||||||
seealso:
|
|
||||||
- module: openssl_certificate
|
|
||||||
description: Can be used to request certificates from ECS, with C(provider=entrust).
|
|
||||||
- module: ecs_certificate
|
|
||||||
description: Can be used to request a Certificate from ECS using a verified domain.
|
|
||||||
extends_documentation_fragment:
|
|
||||||
- ecs_credential
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Request domain validation using email validation for client ID of 2.
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: ansible.com
|
|
||||||
client_id: 2
|
|
||||||
verification_method: email
|
|
||||||
verification_email: admin@ansible.com
|
|
||||||
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-client.key
|
|
||||||
|
|
||||||
- name: Request domain validation using DNS. If domain is already valid,
|
|
||||||
request revalidation if expires within 90 days
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: ansible.com
|
|
||||||
verification_method: dns
|
|
||||||
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-client.key
|
|
||||||
|
|
||||||
- name: Request domain validation using web server validation, and revalidate
|
|
||||||
if fewer than 60 days remaining of EV eligibility.
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: ansible.com
|
|
||||||
verification_method: web_server
|
|
||||||
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-client.key
|
|
||||||
|
|
||||||
- name: Request domain validation using manual validation.
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: ansible.com
|
|
||||||
verification_method: manual
|
|
||||||
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-client.key
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
domain_status:
|
|
||||||
description: Status of the current domain. Will be one of C(APPROVED), C(DECLINED), C(CANCELLED), C(INITIAL_VERIFICATION), C(DECLINED), C(CANCELLED),
|
|
||||||
C(RE_VERIFICATION), C(EXPIRED), C(EXPIRING)
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: APPROVED
|
|
||||||
verification_method:
|
|
||||||
description: Verification method used to request the domain validation. If C(changed) will be the same as I(verification_method) input parameter.
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: dns
|
|
||||||
file_location:
|
|
||||||
description: The location that ECS will be expecting to be able to find the file for domain verification, containing the contents of I(file_contents).
|
|
||||||
returned: I(verification_method) is C(web_server)
|
|
||||||
type: str
|
|
||||||
sample: http://ansible.com/.well-known/pki-validation/abcd.txt
|
|
||||||
file_contents:
|
|
||||||
description: The contents of the file that ECS will be expecting to find at C(file_location).
|
|
||||||
returned: I(verification_method) is C(web_server)
|
|
||||||
type: str
|
|
||||||
sample: AB23CD41432522FF2526920393982FAB
|
|
||||||
emails:
|
|
||||||
description:
|
|
||||||
- The list of emails used to request validation of this domain.
|
|
||||||
- Domains requested using this module will only have a list of size 1.
|
|
||||||
returned: I(verification_method) is C(email)
|
|
||||||
type: list
|
|
||||||
sample: [ admin@ansible.com, administrator@ansible.com ]
|
|
||||||
dns_location:
|
|
||||||
description: The location that ECS will be expecting to be able to find the DNS entry for domain verification, containing the contents of I(dns_contents).
|
|
||||||
returned: changed and if I(verification_method) is C(dns)
|
|
||||||
type: str
|
|
||||||
sample: _pki-validation.ansible.com
|
|
||||||
dns_contents:
|
|
||||||
description: The value that ECS will be expecting to find in the DNS record located at I(dns_location).
|
|
||||||
returned: changed and if I(verification_method) is C(dns)
|
|
||||||
type: str
|
|
||||||
sample: AB23CD41432522FF2526920393982FAB
|
|
||||||
dns_resource_type:
|
|
||||||
description: The type of resource record that ECS will be expecting for the DNS record located at I(dns_location).
|
|
||||||
returned: changed and if I(verification_method) is C(dns)
|
|
||||||
type: str
|
|
||||||
sample: TXT
|
|
||||||
client_id:
|
|
||||||
description: Client ID that the domain belongs to. If the input value I(client_id) is specified, this will always be the same as I(client_id)
|
|
||||||
returned: changed or success
|
|
||||||
type: int
|
|
||||||
sample: 1
|
|
||||||
ov_eligible:
|
|
||||||
description: Whether the domain is eligible for submission of "OV" certificates. Will never be C(false) if I(ov_eligible) is C(true)
|
|
||||||
returned: success and I(domain_status) is C(APPROVED), C(RE_VERIFICATION), C(EXPIRING), or C(EXPIRED).
|
|
||||||
type: bool
|
|
||||||
sample: true
|
|
||||||
ov_days_remaining:
|
|
||||||
description: The number of days the domain remains eligible for submission of "OV" certificates. Will never be less than the value of I(ev_days_remaining)
|
|
||||||
returned: success and I(ov_eligible) is C(true) and I(domain_status) is C(APPROVED), C(RE_VERIFICATION) or C(EXPIRING).
|
|
||||||
type: int
|
|
||||||
sample: 129
|
|
||||||
ev_eligible:
|
|
||||||
description: Whether the domain is eligible for submission of "EV" certificates. Will never be C(true) if I(ov_eligible) is C(false)
|
|
||||||
returned: success and I(domain_status) is C(APPROVED), C(RE_VERIFICATION) or C(EXPIRING), or C(EXPIRED).
|
|
||||||
type: bool
|
|
||||||
sample: true
|
|
||||||
ev_days_remaining:
|
|
||||||
description: The number of days the domain remains eligible for submission of "EV" certificates. Will never be greater than the value of
|
|
||||||
I(ov_days_remaining)
|
|
||||||
returned: success and I(ev_eligible) is C(true) and I(domain_status) is C(APPROVED), C(RE_VERIFICATION) or C(EXPIRING).
|
|
||||||
type: int
|
|
||||||
sample: 94
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
from ansible.module_utils.ecs.api import (
|
|
||||||
ecs_client_argument_spec,
|
|
||||||
ECSClient,
|
|
||||||
RestOperationException,
|
|
||||||
SessionConfigurationException,
|
|
||||||
)
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import time
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
from ansible.module_utils._text import to_native
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_days_remaining(expiry_date):
|
|
||||||
days_remaining = None
|
|
||||||
if expiry_date:
|
|
||||||
expiry_datetime = datetime.datetime.strptime(expiry_date, '%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
days_remaining = (expiry_datetime - datetime.datetime.now()).days
|
|
||||||
return days_remaining
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def ecs_domain_argument_spec():
|
|
||||||
return dict(
|
|
||||||
client_id=dict(type='int', default=1),
|
|
||||||
domain_name=dict(type='str', required=True),
|
|
||||||
verification_method=dict(type='str', required=True, choices=['dns', 'email', 'manual', 'web_server']),
|
|
||||||
verification_email=dict(type='str'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
ecs_argument_spec = ecs_client_argument_spec()
|
|
||||||
ecs_argument_spec.update(ecs_domain_argument_spec())
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=ecs_argument_spec,
|
|
||||||
supports_check_mode=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if module.params['verification_email'] and module.params['verification_method'] != 'email':
|
|
||||||
module.fail_json(msg='The verification_email field is invalid when verification_method="{0}".'.format(module.params['verification_method']))
|
|
||||||
|
|
||||||
domain = EcsDomain(module)
|
|
||||||
domain.request_domain(module)
|
|
||||||
result = domain.dump()
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,371 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# coding: utf-8 -*-
|
|
||||||
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: get_certificate
|
|
||||||
author: "John Westcott IV (@john-westcott-iv)"
|
|
||||||
version_added: "2.8"
|
|
||||||
short_description: Get a certificate from a host:port
|
|
||||||
description:
|
|
||||||
- Makes a secure connection and returns information about the presented certificate
|
|
||||||
- The module can use the cryptography Python library, or the pyOpenSSL Python
|
|
||||||
library. By default, it tries to detect which one is available. This can be
|
|
||||||
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL
|
|
||||||
backend was deprecated in Ansible 2.9 and will be removed in Ansible 2.13."
|
|
||||||
options:
|
|
||||||
host:
|
|
||||||
description:
|
|
||||||
- The host to get the cert for (IP is fine)
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
ca_cert:
|
|
||||||
description:
|
|
||||||
- A PEM file containing one or more root certificates; if present, the cert will be validated against these root certs.
|
|
||||||
- Note that this only validates the certificate is signed by the chain; not that the cert is valid for the host presenting it.
|
|
||||||
type: path
|
|
||||||
port:
|
|
||||||
description:
|
|
||||||
- The port to connect to
|
|
||||||
type: int
|
|
||||||
required: true
|
|
||||||
proxy_host:
|
|
||||||
description:
|
|
||||||
- Proxy host used when get a certificate.
|
|
||||||
type: str
|
|
||||||
version_added: 2.9
|
|
||||||
proxy_port:
|
|
||||||
description:
|
|
||||||
- Proxy port used when get a certificate.
|
|
||||||
type: int
|
|
||||||
default: 8080
|
|
||||||
version_added: 2.9
|
|
||||||
timeout:
|
|
||||||
description:
|
|
||||||
- The timeout in seconds
|
|
||||||
type: int
|
|
||||||
default: 10
|
|
||||||
select_crypto_backend:
|
|
||||||
description:
|
|
||||||
- Determines which crypto backend to use.
|
|
||||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
|
||||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
|
||||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
|
||||||
type: str
|
|
||||||
default: auto
|
|
||||||
choices: [ auto, cryptography, pyopenssl ]
|
|
||||||
version_added: "2.9"
|
|
||||||
|
|
||||||
notes:
|
|
||||||
- When using ca_cert on OS X it has been reported that in some conditions the validate will always succeed.
|
|
||||||
|
|
||||||
requirements:
|
|
||||||
- "python >= 2.7 when using C(proxy_host)"
|
|
||||||
- "cryptography >= 1.6 or pyOpenSSL >= 0.15"
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
cert:
|
|
||||||
description: The certificate retrieved from the port
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
expired:
|
|
||||||
description: Boolean indicating if the cert is expired
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
extensions:
|
|
||||||
description: Extensions applied to the cert
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
contains:
|
|
||||||
critical:
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
description: Whether the extension is critical.
|
|
||||||
asn1_data:
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
description: The Base64 encoded ASN.1 content of the extnesion.
|
|
||||||
name:
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
description: The extension's name.
|
|
||||||
issuer:
|
|
||||||
description: Information about the issuer of the cert
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
not_after:
|
|
||||||
description: Expiration date of the cert
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
not_before:
|
|
||||||
description: Issue date of the cert
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
serial_number:
|
|
||||||
description: The serial number of the cert
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
signature_algorithm:
|
|
||||||
description: The algorithm used to sign the cert
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
subject:
|
|
||||||
description: Information about the subject of the cert (OU, CN, etc)
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
version:
|
|
||||||
description: The version number of the certificate
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = '''
|
|
||||||
- name: Get the cert from an RDP port
|
|
||||||
get_certificate:
|
|
||||||
host: "1.2.3.4"
|
|
||||||
port: 3389
|
|
||||||
delegate_to: localhost
|
|
||||||
run_once: true
|
|
||||||
register: cert
|
|
||||||
|
|
||||||
- name: Get a cert from an https port
|
|
||||||
get_certificate:
|
|
||||||
host: "www.google.com"
|
|
||||||
port: 443
|
|
||||||
delegate_to: localhost
|
|
||||||
run_once: true
|
|
||||||
register: cert
|
|
||||||
|
|
||||||
- name: How many days until cert expires
|
|
||||||
debug:
|
|
||||||
msg: "cert expires in: {{ expire_days }} days."
|
|
||||||
vars:
|
|
||||||
expire_days: "{{ (( cert.not_after | to_datetime('%Y%m%d%H%M%SZ')) - (ansible_date_time.iso8601 | to_datetime('%Y-%m-%dT%H:%M:%SZ')) ).days }}"
|
|
||||||
'''
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
from ansible.module_utils._text import to_bytes
|
|
||||||
from ansible.module_utils import crypto as crypto_utils
|
|
||||||
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
from os.path import isfile
|
|
||||||
from socket import setdefaulttimeout, socket
|
|
||||||
from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_OPTIONAL
|
|
||||||
|
|
||||||
import atexit
|
|
||||||
import base64
|
|
||||||
import datetime
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
|
||||||
|
|
||||||
CREATE_DEFAULT_CONTEXT_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
from ssl import create_default_context
|
|
||||||
except ImportError:
|
|
||||||
CREATE_DEFAULT_CONTEXT_IMP_ERR = traceback.format_exc()
|
|
||||||
HAS_CREATE_DEFAULT_CONTEXT = False
|
|
||||||
else:
|
|
||||||
HAS_CREATE_DEFAULT_CONTEXT = True
|
|
||||||
|
|
||||||
PYOPENSSL_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import OpenSSL
|
|
||||||
from OpenSSL import crypto
|
|
||||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
|
||||||
except ImportError:
|
|
||||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
|
||||||
PYOPENSSL_FOUND = False
|
|
||||||
else:
|
|
||||||
PYOPENSSL_FOUND = True
|
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
import cryptography.exceptions
|
|
||||||
import cryptography.x509
|
|
||||||
from cryptography.hazmat.backends import default_backend as cryptography_backend
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
ca_cert=dict(type='path'),
|
|
||||||
host=dict(type='str', required=True),
|
|
||||||
port=dict(type='int', required=True),
|
|
||||||
proxy_host=dict(type='str'),
|
|
||||||
proxy_port=dict(type='int', default=8080),
|
|
||||||
timeout=dict(type='int', default=10),
|
|
||||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
ca_cert = module.params.get('ca_cert')
|
|
||||||
host = module.params.get('host')
|
|
||||||
port = module.params.get('port')
|
|
||||||
proxy_host = module.params.get('proxy_host')
|
|
||||||
proxy_port = module.params.get('proxy_port')
|
|
||||||
timeout = module.params.get('timeout')
|
|
||||||
|
|
||||||
backend = module.params.get('select_crypto_backend')
|
|
||||||
if backend == 'auto':
|
|
||||||
# Detection what is possible
|
|
||||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
|
||||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
|
||||||
|
|
||||||
# First try cryptography, then pyOpenSSL
|
|
||||||
if can_use_cryptography:
|
|
||||||
backend = 'cryptography'
|
|
||||||
elif can_use_pyopenssl:
|
|
||||||
backend = 'pyopenssl'
|
|
||||||
|
|
||||||
# Success?
|
|
||||||
if backend == 'auto':
|
|
||||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
|
||||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
|
||||||
MINIMAL_PYOPENSSL_VERSION))
|
|
||||||
|
|
||||||
if backend == 'pyopenssl':
|
|
||||||
if not PYOPENSSL_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
|
||||||
exception=PYOPENSSL_IMP_ERR)
|
|
||||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13')
|
|
||||||
elif backend == 'cryptography':
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
|
|
||||||
result = dict(
|
|
||||||
changed=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if timeout:
|
|
||||||
setdefaulttimeout(timeout)
|
|
||||||
|
|
||||||
if ca_cert:
|
|
||||||
if not isfile(ca_cert):
|
|
||||||
module.fail_json(msg="ca_cert file does not exist")
|
|
||||||
|
|
||||||
if proxy_host:
|
|
||||||
if not HAS_CREATE_DEFAULT_CONTEXT:
|
|
||||||
module.fail_json(msg='To use proxy_host, you must run the get_certificate module with Python 2.7 or newer.',
|
|
||||||
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR)
|
|
||||||
|
|
||||||
try:
|
|
||||||
connect = "CONNECT %s:%s HTTP/1.0\r\n\r\n" % (host, port)
|
|
||||||
sock = socket()
|
|
||||||
atexit.register(sock.close)
|
|
||||||
sock.connect((proxy_host, proxy_port))
|
|
||||||
sock.send(connect.encode())
|
|
||||||
sock.recv(8192)
|
|
||||||
|
|
||||||
ctx = create_default_context()
|
|
||||||
ctx.check_hostname = False
|
|
||||||
ctx.verify_mode = CERT_NONE
|
|
||||||
|
|
||||||
if ca_cert:
|
|
||||||
ctx.verify_mode = CERT_OPTIONAL
|
|
||||||
ctx.load_verify_locations(cafile=ca_cert)
|
|
||||||
|
|
||||||
cert = ctx.wrap_socket(sock, server_hostname=host).getpeercert(True)
|
|
||||||
cert = DER_cert_to_PEM_cert(cert)
|
|
||||||
except Exception as e:
|
|
||||||
module.fail_json(msg="Failed to get cert from port with error: {0}".format(e))
|
|
||||||
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
cert = get_server_certificate((host, port), ca_certs=ca_cert)
|
|
||||||
except Exception as e:
|
|
||||||
module.fail_json(msg="Failed to get cert from port with error: {0}".format(e))
|
|
||||||
|
|
||||||
result['cert'] = cert
|
|
||||||
|
|
||||||
if backend == 'pyopenssl':
|
|
||||||
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
|
|
||||||
result['subject'] = {}
|
|
||||||
for component in x509.get_subject().get_components():
|
|
||||||
result['subject'][component[0]] = component[1]
|
|
||||||
|
|
||||||
result['expired'] = x509.has_expired()
|
|
||||||
|
|
||||||
result['extensions'] = []
|
|
||||||
extension_count = x509.get_extension_count()
|
|
||||||
for index in range(0, extension_count):
|
|
||||||
extension = x509.get_extension(index)
|
|
||||||
result['extensions'].append({
|
|
||||||
'critical': extension.get_critical(),
|
|
||||||
'asn1_data': extension.get_data(),
|
|
||||||
'name': extension.get_short_name(),
|
|
||||||
})
|
|
||||||
|
|
||||||
result['issuer'] = {}
|
|
||||||
for component in x509.get_issuer().get_components():
|
|
||||||
result['issuer'][component[0]] = component[1]
|
|
||||||
|
|
||||||
result['not_after'] = x509.get_notAfter()
|
|
||||||
result['not_before'] = x509.get_notBefore()
|
|
||||||
|
|
||||||
result['serial_number'] = x509.get_serial_number()
|
|
||||||
result['signature_algorithm'] = x509.get_signature_algorithm()
|
|
||||||
|
|
||||||
result['version'] = x509.get_version()
|
|
||||||
|
|
||||||
elif backend == 'cryptography':
|
|
||||||
x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography_backend())
|
|
||||||
result['subject'] = {}
|
|
||||||
for attribute in x509.subject:
|
|
||||||
result['subject'][crypto_utils.cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value
|
|
||||||
|
|
||||||
result['expired'] = x509.not_valid_after < datetime.datetime.utcnow()
|
|
||||||
|
|
||||||
result['extensions'] = []
|
|
||||||
for dotted_number, entry in crypto_utils.cryptography_get_extensions_from_cert(x509).items():
|
|
||||||
oid = cryptography.x509.oid.ObjectIdentifier(dotted_number)
|
|
||||||
result['extensions'].append({
|
|
||||||
'critical': entry['critical'],
|
|
||||||
'asn1_data': base64.b64decode(entry['value']),
|
|
||||||
'name': crypto_utils.cryptography_oid_to_name(oid, short=True),
|
|
||||||
})
|
|
||||||
|
|
||||||
result['issuer'] = {}
|
|
||||||
for attribute in x509.issuer:
|
|
||||||
result['issuer'][crypto_utils.cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value
|
|
||||||
|
|
||||||
result['not_after'] = x509.not_valid_after.strftime('%Y%m%d%H%M%SZ')
|
|
||||||
result['not_before'] = x509.not_valid_before.strftime('%Y%m%d%H%M%SZ')
|
|
||||||
|
|
||||||
result['serial_number'] = x509.serial_number
|
|
||||||
result['signature_algorithm'] = crypto_utils.cryptography_oid_to_name(x509.signature_algorithm_oid)
|
|
||||||
|
|
||||||
# We need the -1 offset to get the same values as pyOpenSSL
|
|
||||||
if x509.version == cryptography.x509.Version.v1:
|
|
||||||
result['version'] = 1 - 1
|
|
||||||
elif x509.version == cryptography.x509.Version.v3:
|
|
||||||
result['version'] = 3 - 1
|
|
||||||
else:
|
|
||||||
result['version'] = "unknown"
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,794 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# Copyright (c) 2017 Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: luks_device
|
|
||||||
|
|
||||||
short_description: Manage encrypted (LUKS) devices
|
|
||||||
|
|
||||||
version_added: "2.8"
|
|
||||||
|
|
||||||
description:
|
|
||||||
- "Module manages L(LUKS,https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup)
|
|
||||||
on given device. Supports creating, destroying, opening and closing of
|
|
||||||
LUKS container and adding or removing new keys and passphrases."
|
|
||||||
|
|
||||||
options:
|
|
||||||
device:
|
|
||||||
description:
|
|
||||||
- "Device to work with (e.g. C(/dev/sda1)). Needed in most cases.
|
|
||||||
Can be omitted only when I(state=closed) together with I(name)
|
|
||||||
is provided."
|
|
||||||
type: str
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- "Desired state of the LUKS container. Based on its value creates,
|
|
||||||
destroys, opens or closes the LUKS container on a given device."
|
|
||||||
- "I(present) will create LUKS container unless already present.
|
|
||||||
Requires I(device) and either I(keyfile) or I(passphrase) options
|
|
||||||
to be provided."
|
|
||||||
- "I(absent) will remove existing LUKS container if it exists.
|
|
||||||
Requires I(device) or I(name) to be specified."
|
|
||||||
- "I(opened) will unlock the LUKS container. If it does not exist
|
|
||||||
it will be created first.
|
|
||||||
Requires I(device) and either I(keyfile) or I(passphrase)
|
|
||||||
to be specified. Use the I(name) option to set the name of
|
|
||||||
the opened container. Otherwise the name will be
|
|
||||||
generated automatically and returned as a part of the
|
|
||||||
result."
|
|
||||||
- "I(closed) will lock the LUKS container. However if the container
|
|
||||||
does not exist it will be created.
|
|
||||||
Requires I(device) and either I(keyfile) or I(passphrase)
|
|
||||||
options to be provided. If container does already exist
|
|
||||||
I(device) or I(name) will suffice."
|
|
||||||
type: str
|
|
||||||
default: present
|
|
||||||
choices: [present, absent, opened, closed]
|
|
||||||
name:
|
|
||||||
description:
|
|
||||||
- "Sets container name when I(state=opened). Can be used
|
|
||||||
instead of I(device) when closing the existing container
|
|
||||||
(i.e. when I(state=closed))."
|
|
||||||
type: str
|
|
||||||
keyfile:
|
|
||||||
description:
|
|
||||||
- "Used to unlock the container. Either a I(keyfile) or a
|
|
||||||
I(passphrase) is needed for most of the operations. Parameter
|
|
||||||
value is the path to the keyfile with the passphrase."
|
|
||||||
- "BEWARE that working with keyfiles in plaintext is dangerous.
|
|
||||||
Make sure that they are protected."
|
|
||||||
type: path
|
|
||||||
passphrase:
|
|
||||||
description:
|
|
||||||
- "Used to unlock the container. Either a I(passphrase) or a
|
|
||||||
I(keyfile) is needed for most of the operations. Parameter
|
|
||||||
value is a string with the passphrase."
|
|
||||||
type: str
|
|
||||||
version_added: '2.10'
|
|
||||||
keysize:
|
|
||||||
description:
|
|
||||||
- "Sets the key size only if LUKS container does not exist."
|
|
||||||
type: int
|
|
||||||
version_added: '2.10'
|
|
||||||
new_keyfile:
|
|
||||||
description:
|
|
||||||
- "Adds additional key to given container on I(device).
|
|
||||||
Needs I(keyfile) or I(passphrase) option for authorization.
|
|
||||||
LUKS container supports up to 8 keyslots. Parameter value
|
|
||||||
is the path to the keyfile with the passphrase."
|
|
||||||
- "NOTE that adding additional keys is *not idempotent*.
|
|
||||||
A new keyslot will be used even if another keyslot already
|
|
||||||
exists for this keyfile."
|
|
||||||
- "BEWARE that working with keyfiles in plaintext is dangerous.
|
|
||||||
Make sure that they are protected."
|
|
||||||
type: path
|
|
||||||
new_passphrase:
|
|
||||||
description:
|
|
||||||
- "Adds additional passphrase to given container on I(device).
|
|
||||||
Needs I(keyfile) or I(passphrase) option for authorization. LUKS
|
|
||||||
container supports up to 8 keyslots. Parameter value is a string
|
|
||||||
with the new passphrase."
|
|
||||||
- "NOTE that adding additional passphrase is *not idempotent*. A
|
|
||||||
new keyslot will be used even if another keyslot already exists
|
|
||||||
for this passphrase."
|
|
||||||
type: str
|
|
||||||
version_added: '2.10'
|
|
||||||
remove_keyfile:
|
|
||||||
description:
|
|
||||||
- "Removes given key from the container on I(device). Does not
|
|
||||||
remove the keyfile from filesystem.
|
|
||||||
Parameter value is the path to the keyfile with the passphrase."
|
|
||||||
- "NOTE that removing keys is *not idempotent*. Trying to remove
|
|
||||||
a key which no longer exists results in an error."
|
|
||||||
- "NOTE that to remove the last key from a LUKS container, the
|
|
||||||
I(force_remove_last_key) option must be set to C(yes)."
|
|
||||||
- "BEWARE that working with keyfiles in plaintext is dangerous.
|
|
||||||
Make sure that they are protected."
|
|
||||||
type: path
|
|
||||||
remove_passphrase:
|
|
||||||
description:
|
|
||||||
- "Removes given passphrase from the container on I(device).
|
|
||||||
Parameter value is a string with the passphrase to remove."
|
|
||||||
- "NOTE that removing passphrases is I(not
|
|
||||||
idempotent). Trying to remove a passphrase which no longer
|
|
||||||
exists results in an error."
|
|
||||||
- "NOTE that to remove the last keyslot from a LUKS
|
|
||||||
container, the I(force_remove_last_key) option must be set
|
|
||||||
to C(yes)."
|
|
||||||
type: str
|
|
||||||
version_added: '2.10'
|
|
||||||
force_remove_last_key:
|
|
||||||
description:
|
|
||||||
- "If set to C(yes), allows removing the last key from a container."
|
|
||||||
- "BEWARE that when the last key has been removed from a container,
|
|
||||||
the container can no longer be opened!"
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
label:
|
|
||||||
description:
|
|
||||||
- "This option allow the user to create a LUKS2 format container
|
|
||||||
with label support, respectively to identify the container by
|
|
||||||
label on later usages."
|
|
||||||
- "Will only be used on container creation, or when I(device) is
|
|
||||||
not specified."
|
|
||||||
- "This cannot be specified if I(type) is set to C(luks1)."
|
|
||||||
type: str
|
|
||||||
version_added: "2.10"
|
|
||||||
uuid:
|
|
||||||
description:
|
|
||||||
- "With this option user can identify the LUKS container by UUID."
|
|
||||||
- "Will only be used when I(device) and I(label) are not specified."
|
|
||||||
type: str
|
|
||||||
version_added: "2.10"
|
|
||||||
type:
|
|
||||||
description:
|
|
||||||
- "This option allow the user explicit define the format of LUKS
|
|
||||||
container that wants to work with. Options are C(luks1) or C(luks2)"
|
|
||||||
type: str
|
|
||||||
choices: [luks1, luks2]
|
|
||||||
version_added: "2.10"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
requirements:
|
|
||||||
- "cryptsetup"
|
|
||||||
- "wipefs (when I(state) is C(absent))"
|
|
||||||
- "lsblk"
|
|
||||||
- "blkid (when I(label) or I(uuid) options are used)"
|
|
||||||
|
|
||||||
author: Jan Pokorny (@japokorn)
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = '''
|
|
||||||
|
|
||||||
- name: create LUKS container (remains unchanged if it already exists)
|
|
||||||
luks_device:
|
|
||||||
device: "/dev/loop0"
|
|
||||||
state: "present"
|
|
||||||
keyfile: "/vault/keyfile"
|
|
||||||
|
|
||||||
- name: create LUKS container with a passphrase
|
|
||||||
luks_device:
|
|
||||||
device: "/dev/loop0"
|
|
||||||
state: "present"
|
|
||||||
passphrase: "foo"
|
|
||||||
|
|
||||||
- name: (create and) open the LUKS container; name it "mycrypt"
|
|
||||||
luks_device:
|
|
||||||
device: "/dev/loop0"
|
|
||||||
state: "opened"
|
|
||||||
name: "mycrypt"
|
|
||||||
keyfile: "/vault/keyfile"
|
|
||||||
|
|
||||||
- name: close the existing LUKS container "mycrypt"
|
|
||||||
luks_device:
|
|
||||||
state: "closed"
|
|
||||||
name: "mycrypt"
|
|
||||||
|
|
||||||
- name: make sure LUKS container exists and is closed
|
|
||||||
luks_device:
|
|
||||||
device: "/dev/loop0"
|
|
||||||
state: "closed"
|
|
||||||
keyfile: "/vault/keyfile"
|
|
||||||
|
|
||||||
- name: create container if it does not exist and add new key to it
|
|
||||||
luks_device:
|
|
||||||
device: "/dev/loop0"
|
|
||||||
state: "present"
|
|
||||||
keyfile: "/vault/keyfile"
|
|
||||||
new_keyfile: "/vault/keyfile2"
|
|
||||||
|
|
||||||
- name: add new key to the LUKS container (container has to exist)
|
|
||||||
luks_device:
|
|
||||||
device: "/dev/loop0"
|
|
||||||
keyfile: "/vault/keyfile"
|
|
||||||
new_keyfile: "/vault/keyfile2"
|
|
||||||
|
|
||||||
- name: add new passphrase to the LUKS container
|
|
||||||
luks_device:
|
|
||||||
device: "/dev/loop0"
|
|
||||||
keyfile: "/vault/keyfile"
|
|
||||||
new_passphrase: "foo"
|
|
||||||
|
|
||||||
- name: remove existing keyfile from the LUKS container
|
|
||||||
luks_device:
|
|
||||||
device: "/dev/loop0"
|
|
||||||
remove_keyfile: "/vault/keyfile2"
|
|
||||||
|
|
||||||
- name: remove existing passphrase from the LUKS container
|
|
||||||
luks_device:
|
|
||||||
device: "/dev/loop0"
|
|
||||||
remove_passphrase: "foo"
|
|
||||||
|
|
||||||
- name: completely remove the LUKS container and its contents
|
|
||||||
luks_device:
|
|
||||||
device: "/dev/loop0"
|
|
||||||
state: "absent"
|
|
||||||
|
|
||||||
- name: create a container with label
|
|
||||||
luks_device:
|
|
||||||
device: "/dev/loop0"
|
|
||||||
state: "present"
|
|
||||||
keyfile: "/vault/keyfile"
|
|
||||||
label: personalLabelName
|
|
||||||
|
|
||||||
- name: open the LUKS container based on label without device; name it "mycrypt"
|
|
||||||
luks_device:
|
|
||||||
label: "personalLabelName"
|
|
||||||
state: "opened"
|
|
||||||
name: "mycrypt"
|
|
||||||
keyfile: "/vault/keyfile"
|
|
||||||
|
|
||||||
- name: close container based on UUID
|
|
||||||
luks_device:
|
|
||||||
uuid: 03ecd578-fad4-4e6c-9348-842e3e8fa340
|
|
||||||
state: "closed"
|
|
||||||
name: "mycrypt"
|
|
||||||
|
|
||||||
- name: create a container using luks2 format
|
|
||||||
luks_device:
|
|
||||||
device: "/dev/loop0"
|
|
||||||
state: "present"
|
|
||||||
keyfile: "/vault/keyfile"
|
|
||||||
type: luks2
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
name:
|
|
||||||
description:
|
|
||||||
When I(state=opened) returns (generated or given) name
|
|
||||||
of LUKS container. Returns None if no name is supplied.
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: "luks-c1da9a58-2fde-4256-9d9f-6ab008b4dd1b"
|
|
||||||
'''
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import stat
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
|
|
||||||
RETURN_CODE = 0
|
|
||||||
STDOUT = 1
|
|
||||||
STDERR = 2
|
|
||||||
|
|
||||||
# used to get <luks-name> out of lsblk output in format 'crypt <luks-name>'
|
|
||||||
# regex takes care of any possible blank characters
|
|
||||||
LUKS_NAME_REGEX = re.compile(r'\s*crypt\s+([^\s]*)\s*')
|
|
||||||
# used to get </luks/device> out of lsblk output
|
|
||||||
# in format 'device: </luks/device>'
|
|
||||||
LUKS_DEVICE_REGEX = re.compile(r'\s*device:\s+([^\s]*)\s*')
|
|
||||||
|
|
||||||
|
|
||||||
class Handler(object):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
self._module = module
|
|
||||||
self._lsblk_bin = self._module.get_bin_path('lsblk', True)
|
|
||||||
|
|
||||||
def _run_command(self, command, data=None):
|
|
||||||
return self._module.run_command(command, data=data)
|
|
||||||
|
|
||||||
def get_device_by_uuid(self, uuid):
|
|
||||||
''' Returns the device that holds UUID passed by user
|
|
||||||
'''
|
|
||||||
self._blkid_bin = self._module.get_bin_path('blkid', True)
|
|
||||||
uuid = self._module.params['uuid']
|
|
||||||
if uuid is None:
|
|
||||||
return None
|
|
||||||
result = self._run_command([self._blkid_bin, '--uuid', uuid])
|
|
||||||
if result[RETURN_CODE] != 0:
|
|
||||||
return None
|
|
||||||
return result[STDOUT].strip()
|
|
||||||
|
|
||||||
def get_device_by_label(self, label):
|
|
||||||
''' Returns the device that holds label passed by user
|
|
||||||
'''
|
|
||||||
self._blkid_bin = self._module.get_bin_path('blkid', True)
|
|
||||||
label = self._module.params['label']
|
|
||||||
if label is None:
|
|
||||||
return None
|
|
||||||
result = self._run_command([self._blkid_bin, '--label', label])
|
|
||||||
if result[RETURN_CODE] != 0:
|
|
||||||
return None
|
|
||||||
return result[STDOUT].strip()
|
|
||||||
|
|
||||||
def generate_luks_name(self, device):
|
|
||||||
''' Generate name for luks based on device UUID ('luks-<UUID>').
|
|
||||||
Raises ValueError when obtaining of UUID fails.
|
|
||||||
'''
|
|
||||||
result = self._run_command([self._lsblk_bin, '-n', device, '-o', 'UUID'])
|
|
||||||
|
|
||||||
if result[RETURN_CODE] != 0:
|
|
||||||
raise ValueError('Error while generating LUKS name for %s: %s'
|
|
||||||
% (device, result[STDERR]))
|
|
||||||
dev_uuid = result[STDOUT].strip()
|
|
||||||
return 'luks-%s' % dev_uuid
|
|
||||||
|
|
||||||
|
|
||||||
class CryptHandler(Handler):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(CryptHandler, self).__init__(module)
|
|
||||||
self._cryptsetup_bin = self._module.get_bin_path('cryptsetup', True)
|
|
||||||
|
|
||||||
def get_container_name_by_device(self, device):
|
|
||||||
''' obtain LUKS container name based on the device where it is located
|
|
||||||
return None if not found
|
|
||||||
raise ValueError if lsblk command fails
|
|
||||||
'''
|
|
||||||
result = self._run_command([self._lsblk_bin, device, '-nlo', 'type,name'])
|
|
||||||
if result[RETURN_CODE] != 0:
|
|
||||||
raise ValueError('Error while obtaining LUKS name for %s: %s'
|
|
||||||
% (device, result[STDERR]))
|
|
||||||
|
|
||||||
m = LUKS_NAME_REGEX.search(result[STDOUT])
|
|
||||||
|
|
||||||
try:
|
|
||||||
name = m.group(1)
|
|
||||||
except AttributeError:
|
|
||||||
name = None
|
|
||||||
return name
|
|
||||||
|
|
||||||
def get_container_device_by_name(self, name):
|
|
||||||
''' obtain device name based on the LUKS container name
|
|
||||||
return None if not found
|
|
||||||
raise ValueError if lsblk command fails
|
|
||||||
'''
|
|
||||||
# apparently each device can have only one LUKS container on it
|
|
||||||
result = self._run_command([self._cryptsetup_bin, 'status', name])
|
|
||||||
if result[RETURN_CODE] != 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
m = LUKS_DEVICE_REGEX.search(result[STDOUT])
|
|
||||||
device = m.group(1)
|
|
||||||
return device
|
|
||||||
|
|
||||||
def is_luks(self, device):
|
|
||||||
''' check if the LUKS container does exist
|
|
||||||
'''
|
|
||||||
result = self._run_command([self._cryptsetup_bin, 'isLuks', device])
|
|
||||||
return result[RETURN_CODE] == 0
|
|
||||||
|
|
||||||
def run_luks_create(self, device, keyfile, passphrase, keysize):
|
|
||||||
# create a new luks container; use batch mode to auto confirm
|
|
||||||
luks_type = self._module.params['type']
|
|
||||||
label = self._module.params['label']
|
|
||||||
|
|
||||||
options = []
|
|
||||||
if keysize is not None:
|
|
||||||
options.append('--key-size=' + str(keysize))
|
|
||||||
if label is not None:
|
|
||||||
options.extend(['--label', label])
|
|
||||||
luks_type = 'luks2'
|
|
||||||
if luks_type is not None:
|
|
||||||
options.extend(['--type', luks_type])
|
|
||||||
|
|
||||||
args = [self._cryptsetup_bin, 'luksFormat']
|
|
||||||
args.extend(options)
|
|
||||||
args.extend(['-q', device])
|
|
||||||
if keyfile:
|
|
||||||
args.append(keyfile)
|
|
||||||
|
|
||||||
result = self._run_command(args, data=passphrase)
|
|
||||||
if result[RETURN_CODE] != 0:
|
|
||||||
raise ValueError('Error while creating LUKS on %s: %s'
|
|
||||||
% (device, result[STDERR]))
|
|
||||||
|
|
||||||
def run_luks_open(self, device, keyfile, passphrase, name):
|
|
||||||
args = [self._cryptsetup_bin]
|
|
||||||
if keyfile:
|
|
||||||
args.extend(['--key-file', keyfile])
|
|
||||||
args.extend(['open', '--type', 'luks', device, name])
|
|
||||||
|
|
||||||
result = self._run_command(args, data=passphrase)
|
|
||||||
if result[RETURN_CODE] != 0:
|
|
||||||
raise ValueError('Error while opening LUKS container on %s: %s'
|
|
||||||
% (device, result[STDERR]))
|
|
||||||
|
|
||||||
def run_luks_close(self, name):
|
|
||||||
result = self._run_command([self._cryptsetup_bin, 'close', name])
|
|
||||||
if result[RETURN_CODE] != 0:
|
|
||||||
raise ValueError('Error while closing LUKS container %s' % (name))
|
|
||||||
|
|
||||||
def run_luks_remove(self, device):
|
|
||||||
wipefs_bin = self._module.get_bin_path('wipefs', True)
|
|
||||||
|
|
||||||
name = self.get_container_name_by_device(device)
|
|
||||||
if name is not None:
|
|
||||||
self.run_luks_close(name)
|
|
||||||
result = self._run_command([wipefs_bin, '--all', device])
|
|
||||||
if result[RETURN_CODE] != 0:
|
|
||||||
raise ValueError('Error while wiping luks container %s: %s'
|
|
||||||
% (device, result[STDERR]))
|
|
||||||
|
|
||||||
def run_luks_add_key(self, device, keyfile, passphrase, new_keyfile,
|
|
||||||
new_passphrase):
|
|
||||||
''' Add new key from a keyfile or passphrase to given 'device';
|
|
||||||
authentication done using 'keyfile' or 'passphrase'.
|
|
||||||
Raises ValueError when command fails.
|
|
||||||
'''
|
|
||||||
data = []
|
|
||||||
args = [self._cryptsetup_bin, 'luksAddKey', device]
|
|
||||||
|
|
||||||
if keyfile:
|
|
||||||
args.extend(['--key-file', keyfile])
|
|
||||||
else:
|
|
||||||
data.append(passphrase)
|
|
||||||
|
|
||||||
if new_keyfile:
|
|
||||||
args.append(new_keyfile)
|
|
||||||
else:
|
|
||||||
data.extend([new_passphrase, new_passphrase])
|
|
||||||
|
|
||||||
result = self._run_command(args, data='\n'.join(data) or None)
|
|
||||||
if result[RETURN_CODE] != 0:
|
|
||||||
raise ValueError('Error while adding new LUKS keyslot to %s: %s'
|
|
||||||
% (device, result[STDERR]))
|
|
||||||
|
|
||||||
def run_luks_remove_key(self, device, keyfile, passphrase,
|
|
||||||
force_remove_last_key=False):
|
|
||||||
''' Remove key from given device
|
|
||||||
Raises ValueError when command fails
|
|
||||||
'''
|
|
||||||
if not force_remove_last_key:
|
|
||||||
result = self._run_command([self._cryptsetup_bin, 'luksDump', device])
|
|
||||||
if result[RETURN_CODE] != 0:
|
|
||||||
raise ValueError('Error while dumping LUKS header from %s'
|
|
||||||
% (device, ))
|
|
||||||
keyslot_count = 0
|
|
||||||
keyslot_area = False
|
|
||||||
keyslot_re = re.compile(r'^Key Slot [0-9]+: ENABLED')
|
|
||||||
for line in result[STDOUT].splitlines():
|
|
||||||
if line.startswith('Keyslots:'):
|
|
||||||
keyslot_area = True
|
|
||||||
elif line.startswith(' '):
|
|
||||||
# LUKS2 header dumps use human-readable indented output.
|
|
||||||
# Thus we have to look out for 'Keyslots:' and count the
|
|
||||||
# number of indented keyslot numbers.
|
|
||||||
if keyslot_area and line[2] in '0123456789':
|
|
||||||
keyslot_count += 1
|
|
||||||
elif line.startswith('\t'):
|
|
||||||
pass
|
|
||||||
elif keyslot_re.match(line):
|
|
||||||
# LUKS1 header dumps have one line per keyslot with ENABLED
|
|
||||||
# or DISABLED in them. We count such lines with ENABLED.
|
|
||||||
keyslot_count += 1
|
|
||||||
else:
|
|
||||||
keyslot_area = False
|
|
||||||
if keyslot_count < 2:
|
|
||||||
self._module.fail_json(msg="LUKS device %s has less than two active keyslots. "
|
|
||||||
"To be able to remove a key, please set "
|
|
||||||
"`force_remove_last_key` to `yes`." % device)
|
|
||||||
|
|
||||||
args = [self._cryptsetup_bin, 'luksRemoveKey', device, '-q']
|
|
||||||
if keyfile:
|
|
||||||
args.extend(['--key-file', keyfile])
|
|
||||||
result = self._run_command(args, data=passphrase)
|
|
||||||
if result[RETURN_CODE] != 0:
|
|
||||||
raise ValueError('Error while removing LUKS key from %s: %s'
|
|
||||||
% (device, result[STDERR]))
|
|
||||||
|
|
||||||
|
|
||||||
class ConditionsHandler(Handler):
|
|
||||||
|
|
||||||
def __init__(self, module, crypthandler):
|
|
||||||
super(ConditionsHandler, self).__init__(module)
|
|
||||||
self._crypthandler = crypthandler
|
|
||||||
self.device = self.get_device_name()
|
|
||||||
|
|
||||||
def get_device_name(self):
|
|
||||||
device = self._module.params.get('device')
|
|
||||||
label = self._module.params.get('label')
|
|
||||||
uuid = self._module.params.get('uuid')
|
|
||||||
name = self._module.params.get('name')
|
|
||||||
|
|
||||||
if device is None and label is not None:
|
|
||||||
device = self.get_device_by_label(label)
|
|
||||||
elif device is None and uuid is not None:
|
|
||||||
device = self.get_device_by_uuid(uuid)
|
|
||||||
elif device is None and name is not None:
|
|
||||||
device = self._crypthandler.get_container_device_by_name(name)
|
|
||||||
|
|
||||||
return device
|
|
||||||
|
|
||||||
def luks_create(self):
|
|
||||||
return (self.device is not None and
|
|
||||||
(self._module.params['keyfile'] is not None or
|
|
||||||
self._module.params['passphrase'] is not None) and
|
|
||||||
self._module.params['state'] in ('present',
|
|
||||||
'opened',
|
|
||||||
'closed') and
|
|
||||||
not self._crypthandler.is_luks(self.device))
|
|
||||||
|
|
||||||
def opened_luks_name(self):
|
|
||||||
''' If luks is already opened, return its name.
|
|
||||||
If 'name' parameter is specified and differs
|
|
||||||
from obtained value, fail.
|
|
||||||
Return None otherwise
|
|
||||||
'''
|
|
||||||
if self._module.params['state'] != 'opened':
|
|
||||||
return None
|
|
||||||
|
|
||||||
# try to obtain luks name - it may be already opened
|
|
||||||
name = self._crypthandler.get_container_name_by_device(self.device)
|
|
||||||
|
|
||||||
if name is None:
|
|
||||||
# container is not open
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self._module.params['name'] is None:
|
|
||||||
# container is already opened
|
|
||||||
return name
|
|
||||||
|
|
||||||
if name != self._module.params['name']:
|
|
||||||
# the container is already open but with different name:
|
|
||||||
# suspicious. back off
|
|
||||||
self._module.fail_json(msg="LUKS container is already opened "
|
|
||||||
"under different name '%s'." % name)
|
|
||||||
|
|
||||||
# container is opened and the names match
|
|
||||||
return name
|
|
||||||
|
|
||||||
def luks_open(self):
|
|
||||||
if ((self._module.params['keyfile'] is None and
|
|
||||||
self._module.params['passphrase'] is None) or
|
|
||||||
self.device is None or
|
|
||||||
self._module.params['state'] != 'opened'):
|
|
||||||
# conditions for open not fulfilled
|
|
||||||
return False
|
|
||||||
|
|
||||||
name = self.opened_luks_name()
|
|
||||||
|
|
||||||
if name is None:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def luks_close(self):
|
|
||||||
if ((self._module.params['name'] is None and self.device is None) or
|
|
||||||
self._module.params['state'] != 'closed'):
|
|
||||||
# conditions for close not fulfilled
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.device is not None:
|
|
||||||
name = self._crypthandler.get_container_name_by_device(self.device)
|
|
||||||
# successfully getting name based on device means that luks is open
|
|
||||||
luks_is_open = name is not None
|
|
||||||
|
|
||||||
if self._module.params['name'] is not None:
|
|
||||||
self.device = self._crypthandler.get_container_device_by_name(
|
|
||||||
self._module.params['name'])
|
|
||||||
# successfully getting device based on name means that luks is open
|
|
||||||
luks_is_open = self.device is not None
|
|
||||||
|
|
||||||
return luks_is_open
|
|
||||||
|
|
||||||
def luks_add_key(self):
|
|
||||||
if (self.device is None or
|
|
||||||
(self._module.params['keyfile'] is None and
|
|
||||||
self._module.params['passphrase'] is None) or
|
|
||||||
(self._module.params['new_keyfile'] is None and
|
|
||||||
self._module.params['new_passphrase'] is None)):
|
|
||||||
# conditions for adding a key not fulfilled
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self._module.params['state'] == 'absent':
|
|
||||||
self._module.fail_json(msg="Contradiction in setup: Asking to "
|
|
||||||
"add a key to absent LUKS.")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def luks_remove_key(self):
|
|
||||||
if (self.device is None or
|
|
||||||
(self._module.params['remove_keyfile'] is None and
|
|
||||||
self._module.params['remove_passphrase'] is None)):
|
|
||||||
# conditions for removing a key not fulfilled
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self._module.params['state'] == 'absent':
|
|
||||||
self._module.fail_json(msg="Contradiction in setup: Asking to "
|
|
||||||
"remove a key from absent LUKS.")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def luks_remove(self):
|
|
||||||
return (self.device is not None and
|
|
||||||
self._module.params['state'] == 'absent' and
|
|
||||||
self._crypthandler.is_luks(self.device))
|
|
||||||
|
|
||||||
|
|
||||||
def run_module():
|
|
||||||
# available arguments/parameters that a user can pass
|
|
||||||
module_args = dict(
|
|
||||||
state=dict(type='str', default='present', choices=['present', 'absent', 'opened', 'closed']),
|
|
||||||
device=dict(type='str'),
|
|
||||||
name=dict(type='str'),
|
|
||||||
keyfile=dict(type='path'),
|
|
||||||
new_keyfile=dict(type='path'),
|
|
||||||
remove_keyfile=dict(type='path'),
|
|
||||||
passphrase=dict(type='str', no_log=True),
|
|
||||||
new_passphrase=dict(type='str', no_log=True),
|
|
||||||
remove_passphrase=dict(type='str', no_log=True),
|
|
||||||
force_remove_last_key=dict(type='bool', default=False),
|
|
||||||
keysize=dict(type='int'),
|
|
||||||
label=dict(type='str'),
|
|
||||||
uuid=dict(type='str'),
|
|
||||||
type=dict(type='str', choices=['luks1', 'luks2']),
|
|
||||||
)
|
|
||||||
|
|
||||||
mutually_exclusive = [
|
|
||||||
('keyfile', 'passphrase'),
|
|
||||||
('new_keyfile', 'new_passphrase'),
|
|
||||||
('remove_keyfile', 'remove_passphrase')
|
|
||||||
]
|
|
||||||
|
|
||||||
# seed the result dict in the object
|
|
||||||
result = dict(
|
|
||||||
changed=False,
|
|
||||||
name=None
|
|
||||||
)
|
|
||||||
|
|
||||||
module = AnsibleModule(argument_spec=module_args,
|
|
||||||
supports_check_mode=True,
|
|
||||||
mutually_exclusive=mutually_exclusive)
|
|
||||||
|
|
||||||
if module.params['device'] is not None:
|
|
||||||
try:
|
|
||||||
statinfo = os.stat(module.params['device'])
|
|
||||||
mode = statinfo.st_mode
|
|
||||||
if not stat.S_ISBLK(mode) and not stat.S_ISCHR(mode):
|
|
||||||
raise Exception('{0} is not a device'.format(module.params['device']))
|
|
||||||
except Exception as e:
|
|
||||||
module.fail_json(msg=str(e))
|
|
||||||
|
|
||||||
crypt = CryptHandler(module)
|
|
||||||
conditions = ConditionsHandler(module, crypt)
|
|
||||||
|
|
||||||
# conditions not allowed to run
|
|
||||||
if module.params['label'] is not None and module.params['type'] == 'luks1':
|
|
||||||
module.fail_json(msg='You cannot combine type luks1 with the label option.')
|
|
||||||
|
|
||||||
# The conditions are in order to allow more operations in one run.
|
|
||||||
# (e.g. create luks and add a key to it)
|
|
||||||
|
|
||||||
# luks create
|
|
||||||
if conditions.luks_create():
|
|
||||||
if not module.check_mode:
|
|
||||||
try:
|
|
||||||
crypt.run_luks_create(conditions.device,
|
|
||||||
module.params['keyfile'],
|
|
||||||
module.params['passphrase'],
|
|
||||||
module.params['keysize'])
|
|
||||||
except ValueError as e:
|
|
||||||
module.fail_json(msg="luks_device error: %s" % e)
|
|
||||||
result['changed'] = True
|
|
||||||
if module.check_mode:
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
# luks open
|
|
||||||
|
|
||||||
name = conditions.opened_luks_name()
|
|
||||||
if name is not None:
|
|
||||||
result['name'] = name
|
|
||||||
|
|
||||||
if conditions.luks_open():
|
|
||||||
name = module.params['name']
|
|
||||||
if name is None:
|
|
||||||
try:
|
|
||||||
name = crypt.generate_luks_name(conditions.device)
|
|
||||||
except ValueError as e:
|
|
||||||
module.fail_json(msg="luks_device error: %s" % e)
|
|
||||||
if not module.check_mode:
|
|
||||||
try:
|
|
||||||
crypt.run_luks_open(conditions.device,
|
|
||||||
module.params['keyfile'],
|
|
||||||
module.params['passphrase'],
|
|
||||||
name)
|
|
||||||
except ValueError as e:
|
|
||||||
module.fail_json(msg="luks_device error: %s" % e)
|
|
||||||
result['name'] = name
|
|
||||||
result['changed'] = True
|
|
||||||
if module.check_mode:
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
# luks close
|
|
||||||
if conditions.luks_close():
|
|
||||||
if conditions.device is not None:
|
|
||||||
try:
|
|
||||||
name = crypt.get_container_name_by_device(
|
|
||||||
conditions.device)
|
|
||||||
except ValueError as e:
|
|
||||||
module.fail_json(msg="luks_device error: %s" % e)
|
|
||||||
else:
|
|
||||||
name = module.params['name']
|
|
||||||
if not module.check_mode:
|
|
||||||
try:
|
|
||||||
crypt.run_luks_close(name)
|
|
||||||
except ValueError as e:
|
|
||||||
module.fail_json(msg="luks_device error: %s" % e)
|
|
||||||
result['name'] = name
|
|
||||||
result['changed'] = True
|
|
||||||
if module.check_mode:
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
# luks add key
|
|
||||||
if conditions.luks_add_key():
|
|
||||||
if not module.check_mode:
|
|
||||||
try:
|
|
||||||
crypt.run_luks_add_key(conditions.device,
|
|
||||||
module.params['keyfile'],
|
|
||||||
module.params['passphrase'],
|
|
||||||
module.params['new_keyfile'],
|
|
||||||
module.params['new_passphrase'])
|
|
||||||
except ValueError as e:
|
|
||||||
module.fail_json(msg="luks_device error: %s" % e)
|
|
||||||
result['changed'] = True
|
|
||||||
if module.check_mode:
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
# luks remove key
|
|
||||||
if conditions.luks_remove_key():
|
|
||||||
if not module.check_mode:
|
|
||||||
try:
|
|
||||||
last_key = module.params['force_remove_last_key']
|
|
||||||
crypt.run_luks_remove_key(conditions.device,
|
|
||||||
module.params['remove_keyfile'],
|
|
||||||
module.params['remove_passphrase'],
|
|
||||||
force_remove_last_key=last_key)
|
|
||||||
except ValueError as e:
|
|
||||||
module.fail_json(msg="luks_device error: %s" % e)
|
|
||||||
result['changed'] = True
|
|
||||||
if module.check_mode:
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
# luks remove
|
|
||||||
if conditions.luks_remove():
|
|
||||||
if not module.check_mode:
|
|
||||||
try:
|
|
||||||
crypt.run_luks_remove(conditions.device)
|
|
||||||
except ValueError as e:
|
|
||||||
module.fail_json(msg="luks_device error: %s" % e)
|
|
||||||
result['changed'] = True
|
|
||||||
if module.check_mode:
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
# Success - return result
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
run_module()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,590 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {
|
|
||||||
'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'
|
|
||||||
}
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: openssh_cert
|
|
||||||
author: "David Kainz (@lolcube)"
|
|
||||||
version_added: "2.8"
|
|
||||||
short_description: Generate OpenSSH host or user certificates.
|
|
||||||
description:
|
|
||||||
- Generate and regenerate OpenSSH host or user certificates.
|
|
||||||
requirements:
|
|
||||||
- "ssh-keygen"
|
|
||||||
options:
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- Whether the host or user certificate should exist or not, taking action if the state is different from what is stated.
|
|
||||||
type: str
|
|
||||||
default: "present"
|
|
||||||
choices: [ 'present', 'absent' ]
|
|
||||||
type:
|
|
||||||
description:
|
|
||||||
- Whether the module should generate a host or a user certificate.
|
|
||||||
- Required if I(state) is C(present).
|
|
||||||
type: str
|
|
||||||
choices: ['host', 'user']
|
|
||||||
force:
|
|
||||||
description:
|
|
||||||
- Should the certificate be regenerated even if it already exists and is valid.
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- Path of the file containing the certificate.
|
|
||||||
type: path
|
|
||||||
required: true
|
|
||||||
signing_key:
|
|
||||||
description:
|
|
||||||
- The path to the private openssh key that is used for signing the public key in order to generate the certificate.
|
|
||||||
- Required if I(state) is C(present).
|
|
||||||
type: path
|
|
||||||
public_key:
|
|
||||||
description:
|
|
||||||
- The path to the public key that will be signed with the signing key in order to generate the certificate.
|
|
||||||
- Required if I(state) is C(present).
|
|
||||||
type: path
|
|
||||||
valid_from:
|
|
||||||
description:
|
|
||||||
- "The point in time the certificate is valid from. Time can be specified either as relative time or as absolute timestamp.
|
|
||||||
Time will always be interpreted as UTC. Valid formats are: C([+-]timespec | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS | YYYY-MM-DD HH:MM:SS | always)
|
|
||||||
where timespec can be an integer + C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
|
||||||
Note that if using relative time this module is NOT idempotent."
|
|
||||||
- Required if I(state) is C(present).
|
|
||||||
type: str
|
|
||||||
valid_to:
|
|
||||||
description:
|
|
||||||
- "The point in time the certificate is valid to. Time can be specified either as relative time or as absolute timestamp.
|
|
||||||
Time will always be interpreted as UTC. Valid formats are: C([+-]timespec | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS | YYYY-MM-DD HH:MM:SS | forever)
|
|
||||||
where timespec can be an integer + C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
|
||||||
Note that if using relative time this module is NOT idempotent."
|
|
||||||
- Required if I(state) is C(present).
|
|
||||||
type: str
|
|
||||||
valid_at:
|
|
||||||
description:
|
|
||||||
- "Check if the certificate is valid at a certain point in time. If it is not the certificate will be regenerated.
|
|
||||||
Time will always be interpreted as UTC. Mainly to be used with relative timespec for I(valid_from) and / or I(valid_to).
|
|
||||||
Note that if using relative time this module is NOT idempotent."
|
|
||||||
type: str
|
|
||||||
principals:
|
|
||||||
description:
|
|
||||||
- "Certificates may be limited to be valid for a set of principal (user/host) names.
|
|
||||||
By default, generated certificates are valid for all users or hosts."
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
options:
|
|
||||||
description:
|
|
||||||
- "Specify certificate options when signing a key. The option that are valid for user certificates are:"
|
|
||||||
- "C(clear): Clear all enabled permissions. This is useful for clearing the default set of permissions so permissions may be added individually."
|
|
||||||
- "C(force-command=command): Forces the execution of command instead of any shell or
|
|
||||||
command specified by the user when the certificate is used for authentication."
|
|
||||||
- "C(no-agent-forwarding): Disable ssh-agent forwarding (permitted by default)."
|
|
||||||
- "C(no-port-forwarding): Disable port forwarding (permitted by default)."
|
|
||||||
- "C(no-pty Disable): PTY allocation (permitted by default)."
|
|
||||||
- "C(no-user-rc): Disable execution of C(~/.ssh/rc) by sshd (permitted by default)."
|
|
||||||
- "C(no-x11-forwarding): Disable X11 forwarding (permitted by default)"
|
|
||||||
- "C(permit-agent-forwarding): Allows ssh-agent forwarding."
|
|
||||||
- "C(permit-port-forwarding): Allows port forwarding."
|
|
||||||
- "C(permit-pty): Allows PTY allocation."
|
|
||||||
- "C(permit-user-rc): Allows execution of C(~/.ssh/rc) by sshd."
|
|
||||||
- "C(permit-x11-forwarding): Allows X11 forwarding."
|
|
||||||
- "C(source-address=address_list): Restrict the source addresses from which the certificate is considered valid.
|
|
||||||
The C(address_list) is a comma-separated list of one or more address/netmask pairs in CIDR format."
|
|
||||||
- "At present, no options are valid for host keys."
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
identifier:
|
|
||||||
description:
|
|
||||||
- Specify the key identity when signing a public key. The identifier that is logged by the server when the certificate is used for authentication.
|
|
||||||
type: str
|
|
||||||
serial_number:
|
|
||||||
description:
|
|
||||||
- "Specify the certificate serial number.
|
|
||||||
The serial number is logged by the server when the certificate is used for authentication.
|
|
||||||
The certificate serial number may be used in a KeyRevocationList.
|
|
||||||
The serial number may be omitted for checks, but must be specified again for a new certificate.
|
|
||||||
Note: The default value set by ssh-keygen is 0."
|
|
||||||
type: int
|
|
||||||
|
|
||||||
extends_documentation_fragment: files
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = '''
|
|
||||||
# Generate an OpenSSH user certificate that is valid forever and for all users
|
|
||||||
- openssh_cert:
|
|
||||||
type: user
|
|
||||||
signing_key: /path/to/private_key
|
|
||||||
public_key: /path/to/public_key.pub
|
|
||||||
path: /path/to/certificate
|
|
||||||
valid_from: always
|
|
||||||
valid_to: forever
|
|
||||||
|
|
||||||
# Generate an OpenSSH host certificate that is valid for 32 weeks from now and will be regenerated
|
|
||||||
# if it is valid for less than 2 weeks from the time the module is being run
|
|
||||||
- openssh_cert:
|
|
||||||
type: host
|
|
||||||
signing_key: /path/to/private_key
|
|
||||||
public_key: /path/to/public_key.pub
|
|
||||||
path: /path/to/certificate
|
|
||||||
valid_from: +0s
|
|
||||||
valid_to: +32w
|
|
||||||
valid_at: +2w
|
|
||||||
|
|
||||||
# Generate an OpenSSH host certificate that is valid forever and only for example.com and examplehost
|
|
||||||
- openssh_cert:
|
|
||||||
type: host
|
|
||||||
signing_key: /path/to/private_key
|
|
||||||
public_key: /path/to/public_key.pub
|
|
||||||
path: /path/to/certificate
|
|
||||||
valid_from: always
|
|
||||||
valid_to: forever
|
|
||||||
principals:
|
|
||||||
- example.com
|
|
||||||
- examplehost
|
|
||||||
|
|
||||||
# Generate an OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019
|
|
||||||
- openssh_cert:
|
|
||||||
type: host
|
|
||||||
signing_key: /path/to/private_key
|
|
||||||
public_key: /path/to/public_key.pub
|
|
||||||
path: /path/to/certificate
|
|
||||||
valid_from: "2001-01-21"
|
|
||||||
valid_to: "2019-01-21"
|
|
||||||
|
|
||||||
# Generate an OpenSSH user Certificate with clear and force-command option:
|
|
||||||
- openssh_cert:
|
|
||||||
type: user
|
|
||||||
signing_key: /path/to/private_key
|
|
||||||
public_key: /path/to/public_key.pub
|
|
||||||
path: /path/to/certificate
|
|
||||||
valid_from: always
|
|
||||||
valid_to: forever
|
|
||||||
options:
|
|
||||||
- "clear"
|
|
||||||
- "force-command=/tmp/bla/foo"
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
type:
|
|
||||||
description: type of the certificate (host or user)
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: host
|
|
||||||
filename:
|
|
||||||
description: path to the certificate
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: /tmp/certificate-cert.pub
|
|
||||||
info:
|
|
||||||
description: Information about the certificate. Output of C(ssh-keygen -L -f).
|
|
||||||
returned: change or success
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
import os
|
|
||||||
import errno
|
|
||||||
import re
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from datetime import MINYEAR, MAXYEAR
|
|
||||||
from shutil import copy2
|
|
||||||
from shutil import rmtree
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
from ansible.module_utils.crypto import convert_relative_to_datetime
|
|
||||||
from ansible.module_utils._text import to_native
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Certificate(object):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
self.state = module.params['state']
|
|
||||||
self.force = module.params['force']
|
|
||||||
self.type = module.params['type']
|
|
||||||
self.signing_key = module.params['signing_key']
|
|
||||||
self.public_key = module.params['public_key']
|
|
||||||
self.path = module.params['path']
|
|
||||||
self.identifier = module.params['identifier']
|
|
||||||
self.serial_number = module.params['serial_number']
|
|
||||||
self.valid_from = module.params['valid_from']
|
|
||||||
self.valid_to = module.params['valid_to']
|
|
||||||
self.valid_at = module.params['valid_at']
|
|
||||||
self.principals = module.params['principals']
|
|
||||||
self.options = module.params['options']
|
|
||||||
self.changed = False
|
|
||||||
self.check_mode = module.check_mode
|
|
||||||
self.cert_info = {}
|
|
||||||
|
|
||||||
if self.state == 'present':
|
|
||||||
|
|
||||||
if self.options and self.type == "host":
|
|
||||||
module.fail_json(msg="Options can only be used with user certificates.")
|
|
||||||
|
|
||||||
if self.valid_at:
|
|
||||||
self.valid_at = self.valid_at.lstrip()
|
|
||||||
|
|
||||||
self.valid_from = self.valid_from.lstrip()
|
|
||||||
self.valid_to = self.valid_to.lstrip()
|
|
||||||
|
|
||||||
self.ssh_keygen = module.get_bin_path('ssh-keygen', True)
|
|
||||||
|
|
||||||
def generate(self, module):
|
|
||||||
|
|
||||||
if not self.is_valid(module, perms_required=False) or self.force:
|
|
||||||
args = [
|
|
||||||
self.ssh_keygen,
|
|
||||||
'-s', self.signing_key
|
|
||||||
]
|
|
||||||
|
|
||||||
validity = ""
|
|
||||||
|
|
||||||
if not (self.valid_from == "always" and self.valid_to == "forever"):
|
|
||||||
|
|
||||||
if not self.valid_from == "always":
|
|
||||||
timeobj = self.convert_to_datetime(module, self.valid_from)
|
|
||||||
validity += (
|
|
||||||
str(timeobj.year).zfill(4) +
|
|
||||||
str(timeobj.month).zfill(2) +
|
|
||||||
str(timeobj.day).zfill(2) +
|
|
||||||
str(timeobj.hour).zfill(2) +
|
|
||||||
str(timeobj.minute).zfill(2) +
|
|
||||||
str(timeobj.second).zfill(2)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
validity += "19700101010101"
|
|
||||||
|
|
||||||
validity += ":"
|
|
||||||
|
|
||||||
if self.valid_to == "forever":
|
|
||||||
# on ssh-keygen versions that have the year 2038 bug this will cause the datetime to be 2038-01-19T04:14:07
|
|
||||||
timeobj = datetime(MAXYEAR, 12, 31)
|
|
||||||
else:
|
|
||||||
timeobj = self.convert_to_datetime(module, self.valid_to)
|
|
||||||
|
|
||||||
validity += (
|
|
||||||
str(timeobj.year).zfill(4) +
|
|
||||||
str(timeobj.month).zfill(2) +
|
|
||||||
str(timeobj.day).zfill(2) +
|
|
||||||
str(timeobj.hour).zfill(2) +
|
|
||||||
str(timeobj.minute).zfill(2) +
|
|
||||||
str(timeobj.second).zfill(2)
|
|
||||||
)
|
|
||||||
|
|
||||||
args.extend(["-V", validity])
|
|
||||||
|
|
||||||
if self.type == 'host':
|
|
||||||
args.extend(['-h'])
|
|
||||||
|
|
||||||
if self.identifier:
|
|
||||||
args.extend(['-I', self.identifier])
|
|
||||||
else:
|
|
||||||
args.extend(['-I', ""])
|
|
||||||
|
|
||||||
if self.serial_number is not None:
|
|
||||||
args.extend(['-z', str(self.serial_number)])
|
|
||||||
|
|
||||||
if self.principals:
|
|
||||||
args.extend(['-n', ','.join(self.principals)])
|
|
||||||
|
|
||||||
if self.options:
|
|
||||||
for option in self.options:
|
|
||||||
args.extend(['-O'])
|
|
||||||
args.extend([option])
|
|
||||||
|
|
||||||
args.extend(['-P', ''])
|
|
||||||
|
|
||||||
try:
|
|
||||||
temp_directory = tempfile.mkdtemp()
|
|
||||||
copy2(self.public_key, temp_directory)
|
|
||||||
args.extend([temp_directory + "/" + os.path.basename(self.public_key)])
|
|
||||||
module.run_command(args, environ_update=dict(TZ="UTC"), check_rc=True)
|
|
||||||
copy2(temp_directory + "/" + os.path.splitext(os.path.basename(self.public_key))[0] + "-cert.pub", self.path)
|
|
||||||
rmtree(temp_directory, ignore_errors=True)
|
|
||||||
proc = module.run_command([self.ssh_keygen, '-L', '-f', self.path])
|
|
||||||
self.cert_info = proc[1].split()
|
|
||||||
self.changed = True
|
|
||||||
except Exception as e:
|
|
||||||
try:
|
|
||||||
self.remove()
|
|
||||||
rmtree(temp_directory, ignore_errors=True)
|
|
||||||
except OSError as exc:
|
|
||||||
if exc.errno != errno.ENOENT:
|
|
||||||
raise CertificateError(exc)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
module.fail_json(msg="%s" % to_native(e))
|
|
||||||
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
|
||||||
if module.set_fs_attributes_if_different(file_args, False):
|
|
||||||
self.changed = True
|
|
||||||
|
|
||||||
def convert_to_datetime(self, module, timestring):
|
|
||||||
|
|
||||||
if self.is_relative(timestring):
|
|
||||||
result = convert_relative_to_datetime(timestring)
|
|
||||||
if result is None:
|
|
||||||
module.fail_json(
|
|
||||||
msg="'%s' is not a valid time format." % timestring)
|
|
||||||
else:
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
formats = ["%Y-%m-%d",
|
|
||||||
"%Y-%m-%d %H:%M:%S",
|
|
||||||
"%Y-%m-%dT%H:%M:%S",
|
|
||||||
]
|
|
||||||
for fmt in formats:
|
|
||||||
try:
|
|
||||||
return datetime.strptime(timestring, fmt)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
module.fail_json(msg="'%s' is not a valid time format" % timestring)
|
|
||||||
|
|
||||||
def is_relative(self, timestr):
|
|
||||||
if timestr.startswith("+") or timestr.startswith("-"):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_same_datetime(self, datetime_one, datetime_two):
|
|
||||||
|
|
||||||
# This function is for backwards compatibility only because .total_seconds() is new in python2.7
|
|
||||||
def timedelta_total_seconds(time_delta):
|
|
||||||
return (time_delta.microseconds + 0.0 + (time_delta.seconds + time_delta.days * 24 * 3600) * 10 ** 6) / 10 ** 6
|
|
||||||
# try to use .total_ seconds() from python2.7
|
|
||||||
try:
|
|
||||||
return (datetime_one - datetime_two).total_seconds() == 0.0
|
|
||||||
except AttributeError:
|
|
||||||
return timedelta_total_seconds(datetime_one - datetime_two) == 0.0
|
|
||||||
|
|
||||||
def is_valid(self, module, perms_required=True):
|
|
||||||
|
|
||||||
def _check_state():
|
|
||||||
return os.path.exists(self.path)
|
|
||||||
|
|
||||||
if _check_state():
|
|
||||||
proc = module.run_command([self.ssh_keygen, '-L', '-f', self.path], environ_update=dict(TZ="UTC"), check_rc=False)
|
|
||||||
if proc[0] != 0:
|
|
||||||
return False
|
|
||||||
self.cert_info = proc[1].split()
|
|
||||||
principals = re.findall("(?<=Principals:)(.*)(?=Critical)", proc[1], re.S)[0].split()
|
|
||||||
principals = list(map(str.strip, principals))
|
|
||||||
if principals == ["(none)"]:
|
|
||||||
principals = None
|
|
||||||
cert_type = re.findall("( user | host )", proc[1])[0].strip()
|
|
||||||
serial_number = re.search(r"Serial: (\d+)", proc[1]).group(1)
|
|
||||||
validity = re.findall("(from (\\d{4}-\\d{2}-\\d{2}T\\d{2}(:\\d{2}){2}) to (\\d{4}-\\d{2}-\\d{2}T\\d{2}(:\\d{2}){2}))", proc[1])
|
|
||||||
if validity:
|
|
||||||
if validity[0][1]:
|
|
||||||
cert_valid_from = self.convert_to_datetime(module, validity[0][1])
|
|
||||||
if self.is_same_datetime(cert_valid_from, self.convert_to_datetime(module, "1970-01-01 01:01:01")):
|
|
||||||
cert_valid_from = datetime(MINYEAR, 1, 1)
|
|
||||||
else:
|
|
||||||
cert_valid_from = datetime(MINYEAR, 1, 1)
|
|
||||||
|
|
||||||
if validity[0][3]:
|
|
||||||
cert_valid_to = self.convert_to_datetime(module, validity[0][3])
|
|
||||||
if self.is_same_datetime(cert_valid_to, self.convert_to_datetime(module, "2038-01-19 03:14:07")):
|
|
||||||
cert_valid_to = datetime(MAXYEAR, 12, 31)
|
|
||||||
else:
|
|
||||||
cert_valid_to = datetime(MAXYEAR, 12, 31)
|
|
||||||
else:
|
|
||||||
cert_valid_from = datetime(MINYEAR, 1, 1)
|
|
||||||
cert_valid_to = datetime(MAXYEAR, 12, 31)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _check_perms(module):
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
|
||||||
return not module.set_fs_attributes_if_different(file_args, False)
|
|
||||||
|
|
||||||
def _check_serial_number():
|
|
||||||
if self.serial_number is None:
|
|
||||||
return True
|
|
||||||
return self.serial_number == int(serial_number)
|
|
||||||
|
|
||||||
def _check_type():
|
|
||||||
return self.type == cert_type
|
|
||||||
|
|
||||||
def _check_principals():
|
|
||||||
if not principals or not self.principals:
|
|
||||||
return self.principals == principals
|
|
||||||
return set(self.principals) == set(principals)
|
|
||||||
|
|
||||||
def _check_validity(module):
|
|
||||||
if self.valid_from == "always":
|
|
||||||
earliest_time = datetime(MINYEAR, 1, 1)
|
|
||||||
elif self.is_relative(self.valid_from):
|
|
||||||
earliest_time = None
|
|
||||||
else:
|
|
||||||
earliest_time = self.convert_to_datetime(module, self.valid_from)
|
|
||||||
|
|
||||||
if self.valid_to == "forever":
|
|
||||||
last_time = datetime(MAXYEAR, 12, 31)
|
|
||||||
elif self.is_relative(self.valid_to):
|
|
||||||
last_time = None
|
|
||||||
else:
|
|
||||||
last_time = self.convert_to_datetime(module, self.valid_to)
|
|
||||||
|
|
||||||
if earliest_time:
|
|
||||||
if not self.is_same_datetime(earliest_time, cert_valid_from):
|
|
||||||
return False
|
|
||||||
if last_time:
|
|
||||||
if not self.is_same_datetime(last_time, cert_valid_to):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.valid_at:
|
|
||||||
if cert_valid_from <= self.convert_to_datetime(module, self.valid_at) <= cert_valid_to:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if earliest_time and last_time:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
if perms_required and not _check_perms(module):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return _check_type() and _check_principals() and _check_validity(module) and _check_serial_number()
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
|
|
||||||
"""Serialize the object into a dictionary."""
|
|
||||||
|
|
||||||
def filter_keywords(arr, keywords):
|
|
||||||
concated = []
|
|
||||||
string = ""
|
|
||||||
for word in arr:
|
|
||||||
if word in keywords:
|
|
||||||
concated.append(string)
|
|
||||||
string = word
|
|
||||||
else:
|
|
||||||
string += " " + word
|
|
||||||
concated.append(string)
|
|
||||||
# drop the certificate path
|
|
||||||
concated.pop(0)
|
|
||||||
return concated
|
|
||||||
|
|
||||||
def format_cert_info():
|
|
||||||
return filter_keywords(self.cert_info, [
|
|
||||||
"Type:",
|
|
||||||
"Public",
|
|
||||||
"Signing",
|
|
||||||
"Key",
|
|
||||||
"Serial:",
|
|
||||||
"Valid:",
|
|
||||||
"Principals:",
|
|
||||||
"Critical",
|
|
||||||
"Extensions:"])
|
|
||||||
|
|
||||||
if self.state == 'present':
|
|
||||||
result = {
|
|
||||||
'changed': self.changed,
|
|
||||||
'type': self.type,
|
|
||||||
'filename': self.path,
|
|
||||||
'info': format_cert_info(),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
result = {
|
|
||||||
'changed': self.changed,
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def remove(self):
|
|
||||||
"""Remove the resource from the filesystem."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.remove(self.path)
|
|
||||||
self.changed = True
|
|
||||||
except OSError as exc:
|
|
||||||
if exc.errno != errno.ENOENT:
|
|
||||||
raise CertificateError(exc)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
|
||||||
force=dict(type='bool', default=False),
|
|
||||||
type=dict(type='str', choices=['host', 'user']),
|
|
||||||
signing_key=dict(type='path'),
|
|
||||||
public_key=dict(type='path'),
|
|
||||||
path=dict(type='path', required=True),
|
|
||||||
identifier=dict(type='str'),
|
|
||||||
serial_number=dict(type='int'),
|
|
||||||
valid_from=dict(type='str'),
|
|
||||||
valid_to=dict(type='str'),
|
|
||||||
valid_at=dict(type='str'),
|
|
||||||
principals=dict(type='list', elements='str'),
|
|
||||||
options=dict(type='list', elements='str'),
|
|
||||||
),
|
|
||||||
supports_check_mode=True,
|
|
||||||
add_file_common_args=True,
|
|
||||||
required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])],
|
|
||||||
)
|
|
||||||
|
|
||||||
def isBaseDir(path):
|
|
||||||
base_dir = os.path.dirname(path) or '.'
|
|
||||||
if not os.path.isdir(base_dir):
|
|
||||||
module.fail_json(
|
|
||||||
name=base_dir,
|
|
||||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
|
||||||
)
|
|
||||||
if module.params['state'] == "present":
|
|
||||||
isBaseDir(module.params['signing_key'])
|
|
||||||
isBaseDir(module.params['public_key'])
|
|
||||||
|
|
||||||
isBaseDir(module.params['path'])
|
|
||||||
|
|
||||||
certificate = Certificate(module)
|
|
||||||
|
|
||||||
if certificate.state == 'present':
|
|
||||||
|
|
||||||
if module.check_mode:
|
|
||||||
certificate.changed = module.params['force'] or not certificate.is_valid(module)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
certificate.generate(module)
|
|
||||||
except Exception as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
if module.check_mode:
|
|
||||||
certificate.changed = os.path.exists(module.params['path'])
|
|
||||||
if certificate.changed:
|
|
||||||
certificate.cert_info = {}
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
certificate.remove()
|
|
||||||
except Exception as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
result = certificate.dump()
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,493 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {
|
|
||||||
'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'
|
|
||||||
}
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
|
||||||
---
|
|
||||||
module: openssh_keypair
|
|
||||||
author: "David Kainz (@lolcube)"
|
|
||||||
version_added: "2.8"
|
|
||||||
short_description: Generate OpenSSH private and public keys.
|
|
||||||
description:
|
|
||||||
- "This module allows one to (re)generate OpenSSH private and public keys. It uses
|
|
||||||
ssh-keygen to generate keys. One can generate C(rsa), C(dsa), C(rsa1), C(ed25519)
|
|
||||||
or C(ecdsa) private keys."
|
|
||||||
requirements:
|
|
||||||
- "ssh-keygen"
|
|
||||||
options:
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- Whether the private and public keys should exist or not, taking action if the state is different from what is stated.
|
|
||||||
type: str
|
|
||||||
default: present
|
|
||||||
choices: [ present, absent ]
|
|
||||||
size:
|
|
||||||
description:
|
|
||||||
- "Specifies the number of bits in the private key to create. For RSA keys, the minimum size is 1024 bits and the default is 4096 bits.
|
|
||||||
Generally, 2048 bits is considered sufficient. DSA keys must be exactly 1024 bits as specified by FIPS 186-2.
|
|
||||||
For ECDSA keys, size determines the key length by selecting from one of three elliptic curve sizes: 256, 384 or 521 bits.
|
|
||||||
Attempting to use bit lengths other than these three values for ECDSA keys will cause this module to fail.
|
|
||||||
Ed25519 keys have a fixed length and the size will be ignored."
|
|
||||||
type: int
|
|
||||||
type:
|
|
||||||
description:
|
|
||||||
- "The algorithm used to generate the SSH private key. C(rsa1) is for protocol version 1.
|
|
||||||
C(rsa1) is deprecated and may not be supported by every version of ssh-keygen."
|
|
||||||
type: str
|
|
||||||
default: rsa
|
|
||||||
choices: ['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519']
|
|
||||||
force:
|
|
||||||
description:
|
|
||||||
- Should the key be regenerated even if it already exists
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- Name of the files containing the public and private key. The file containing the public key will have the extension C(.pub).
|
|
||||||
type: path
|
|
||||||
required: true
|
|
||||||
comment:
|
|
||||||
description:
|
|
||||||
- Provides a new comment to the public key. When checking if the key is in the correct state this will be ignored.
|
|
||||||
type: str
|
|
||||||
version_added: "2.9"
|
|
||||||
regenerate:
|
|
||||||
description:
|
|
||||||
- Allows to configure in which situations the module is allowed to regenerate private keys.
|
|
||||||
The module will always generate a new key if the destination file does not exist.
|
|
||||||
- By default, the key will be regenerated when it doesn't match the module's options,
|
|
||||||
except when the key cannot be read or the passphrase does not match. Please note that
|
|
||||||
this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence)
|
|
||||||
is specified.
|
|
||||||
- If set to C(never), the module will fail if the key cannot be read or the passphrase
|
|
||||||
isn't matching, and will never regenerate an existing key.
|
|
||||||
- If set to C(fail), the module will fail if the key does not correspond to the module's
|
|
||||||
options.
|
|
||||||
- If set to C(partial_idempotence), the key will be regenerated if it does not conform to
|
|
||||||
the module's options. The key is B(not) regenerated if it cannot be read (broken file),
|
|
||||||
the key is protected by an unknown passphrase, or when they key is not protected by a
|
|
||||||
passphrase, but a passphrase is specified.
|
|
||||||
- If set to C(full_idempotence), the key will be regenerated if it does not conform to the
|
|
||||||
module's options. This is also the case if the key cannot be read (broken file), the key
|
|
||||||
is protected by an unknown passphrase, or when they key is not protected by a passphrase,
|
|
||||||
but a passphrase is specified. Make sure you have a B(backup) when using this option!
|
|
||||||
- If set to C(always), the module will always regenerate the key. This is equivalent to
|
|
||||||
setting I(force) to C(yes).
|
|
||||||
- Note that adjusting the comment and the permissions can be changed without regeneration.
|
|
||||||
Therefore, even for C(never), the task can result in changed.
|
|
||||||
type: str
|
|
||||||
choices:
|
|
||||||
- never
|
|
||||||
- fail
|
|
||||||
- partial_idempotence
|
|
||||||
- full_idempotence
|
|
||||||
- always
|
|
||||||
default: partial_idempotence
|
|
||||||
version_added: '2.10'
|
|
||||||
notes:
|
|
||||||
- In case the ssh key is broken or password protected, the module will fail. Set the I(force) option to C(yes) if you want to regenerate the keypair.
|
|
||||||
|
|
||||||
extends_documentation_fragment: files
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = '''
|
|
||||||
# Generate an OpenSSH keypair with the default values (4096 bits, rsa)
|
|
||||||
- openssh_keypair:
|
|
||||||
path: /tmp/id_ssh_rsa
|
|
||||||
|
|
||||||
# Generate an OpenSSH rsa keypair with a different size (2048 bits)
|
|
||||||
- openssh_keypair:
|
|
||||||
path: /tmp/id_ssh_rsa
|
|
||||||
size: 2048
|
|
||||||
|
|
||||||
# Force regenerate an OpenSSH keypair if it already exists
|
|
||||||
- openssh_keypair:
|
|
||||||
path: /tmp/id_ssh_rsa
|
|
||||||
force: True
|
|
||||||
|
|
||||||
# Generate an OpenSSH keypair with a different algorithm (dsa)
|
|
||||||
- openssh_keypair:
|
|
||||||
path: /tmp/id_ssh_dsa
|
|
||||||
type: dsa
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = '''
|
|
||||||
size:
|
|
||||||
description: Size (in bits) of the SSH private key
|
|
||||||
returned: changed or success
|
|
||||||
type: int
|
|
||||||
sample: 4096
|
|
||||||
type:
|
|
||||||
description: Algorithm used to generate the SSH private key
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: rsa
|
|
||||||
filename:
|
|
||||||
description: Path to the generated SSH private key file
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: /tmp/id_ssh_rsa
|
|
||||||
fingerprint:
|
|
||||||
description: The fingerprint of the key.
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: SHA256:r4YCZxihVjedH2OlfjVGI6Y5xAYtdCwk8VxKyzVyYfM
|
|
||||||
public_key:
|
|
||||||
description: The public key of the generated SSH private key
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: ssh-rsa AAAAB3Nza(...omitted...)veL4E3Xcw== test_key
|
|
||||||
comment:
|
|
||||||
description: The comment of the generated key
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: test@comment
|
|
||||||
'''
|
|
||||||
|
|
||||||
import os
|
|
||||||
import stat
|
|
||||||
import errno
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
from ansible.module_utils._text import to_native
|
|
||||||
|
|
||||||
|
|
||||||
class KeypairError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Keypair(object):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
self.path = module.params['path']
|
|
||||||
self.state = module.params['state']
|
|
||||||
self.force = module.params['force']
|
|
||||||
self.size = module.params['size']
|
|
||||||
self.type = module.params['type']
|
|
||||||
self.comment = module.params['comment']
|
|
||||||
self.changed = False
|
|
||||||
self.check_mode = module.check_mode
|
|
||||||
self.privatekey = None
|
|
||||||
self.fingerprint = {}
|
|
||||||
self.public_key = {}
|
|
||||||
self.regenerate = module.params['regenerate']
|
|
||||||
if self.regenerate == 'always':
|
|
||||||
self.force = True
|
|
||||||
|
|
||||||
if self.type in ('rsa', 'rsa1'):
|
|
||||||
self.size = 4096 if self.size is None else self.size
|
|
||||||
if self.size < 1024:
|
|
||||||
module.fail_json(msg=('For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. '
|
|
||||||
'Attempting to use bit lengths under 1024 will cause the module to fail.'))
|
|
||||||
|
|
||||||
if self.type == 'dsa':
|
|
||||||
self.size = 1024 if self.size is None else self.size
|
|
||||||
if self.size != 1024:
|
|
||||||
module.fail_json(msg=('DSA keys must be exactly 1024 bits as specified by FIPS 186-2.'))
|
|
||||||
|
|
||||||
if self.type == 'ecdsa':
|
|
||||||
self.size = 256 if self.size is None else self.size
|
|
||||||
if self.size not in (256, 384, 521):
|
|
||||||
module.fail_json(msg=('For ECDSA keys, size determines the key length by selecting from '
|
|
||||||
'one of three elliptic curve sizes: 256, 384 or 521 bits. '
|
|
||||||
'Attempting to use bit lengths other than these three values for '
|
|
||||||
'ECDSA keys will cause this module to fail. '))
|
|
||||||
if self.type == 'ed25519':
|
|
||||||
self.size = 256
|
|
||||||
|
|
||||||
def generate(self, module):
|
|
||||||
# generate a keypair
|
|
||||||
if self.force or not self.isPrivateKeyValid(module, perms_required=False):
|
|
||||||
args = [
|
|
||||||
module.get_bin_path('ssh-keygen', True),
|
|
||||||
'-q',
|
|
||||||
'-N', '',
|
|
||||||
'-b', str(self.size),
|
|
||||||
'-t', self.type,
|
|
||||||
'-f', self.path,
|
|
||||||
]
|
|
||||||
|
|
||||||
if self.comment:
|
|
||||||
args.extend(['-C', self.comment])
|
|
||||||
else:
|
|
||||||
args.extend(['-C', ""])
|
|
||||||
|
|
||||||
try:
|
|
||||||
if os.path.exists(self.path) and not os.access(self.path, os.W_OK):
|
|
||||||
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
|
|
||||||
self.changed = True
|
|
||||||
stdin_data = None
|
|
||||||
if os.path.exists(self.path):
|
|
||||||
stdin_data = 'y'
|
|
||||||
module.run_command(args, data=stdin_data)
|
|
||||||
proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path])
|
|
||||||
self.fingerprint = proc[1].split()
|
|
||||||
pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path])
|
|
||||||
self.public_key = pubkey[1].strip('\n')
|
|
||||||
except Exception as e:
|
|
||||||
self.remove()
|
|
||||||
module.fail_json(msg="%s" % to_native(e))
|
|
||||||
|
|
||||||
elif not self.isPublicKeyValid(module, perms_required=False):
|
|
||||||
pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path])
|
|
||||||
pubkey = pubkey[1].strip('\n')
|
|
||||||
try:
|
|
||||||
self.changed = True
|
|
||||||
with open(self.path + ".pub", "w") as pubkey_f:
|
|
||||||
pubkey_f.write(pubkey + '\n')
|
|
||||||
os.chmod(self.path + ".pub", stat.S_IWUSR + stat.S_IRUSR + stat.S_IRGRP + stat.S_IROTH)
|
|
||||||
except IOError:
|
|
||||||
module.fail_json(
|
|
||||||
msg='The public key is missing or does not match the private key. '
|
|
||||||
'Unable to regenerate the public key.')
|
|
||||||
self.public_key = pubkey
|
|
||||||
|
|
||||||
if self.comment:
|
|
||||||
try:
|
|
||||||
if os.path.exists(self.path) and not os.access(self.path, os.W_OK):
|
|
||||||
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
|
|
||||||
args = [module.get_bin_path('ssh-keygen', True),
|
|
||||||
'-q', '-o', '-c', '-C', self.comment, '-f', self.path]
|
|
||||||
module.run_command(args)
|
|
||||||
except IOError:
|
|
||||||
module.fail_json(
|
|
||||||
msg='Unable to update the comment for the public key.')
|
|
||||||
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
|
||||||
if module.set_fs_attributes_if_different(file_args, False):
|
|
||||||
self.changed = True
|
|
||||||
file_args['path'] = file_args['path'] + '.pub'
|
|
||||||
if module.set_fs_attributes_if_different(file_args, False):
|
|
||||||
self.changed = True
|
|
||||||
|
|
||||||
def _check_pass_protected_or_broken_key(self, module):
|
|
||||||
key_state = module.run_command([module.get_bin_path('ssh-keygen', True),
|
|
||||||
'-P', '', '-yf', self.path], check_rc=False)
|
|
||||||
if key_state[0] == 255 or 'is not a public key file' in key_state[2]:
|
|
||||||
return True
|
|
||||||
if 'incorrect passphrase' in key_state[2] or 'load failed' in key_state[2]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def isPrivateKeyValid(self, module, perms_required=True):
|
|
||||||
|
|
||||||
# check if the key is correct
|
|
||||||
def _check_state():
|
|
||||||
return os.path.exists(self.path)
|
|
||||||
|
|
||||||
if not _check_state():
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self._check_pass_protected_or_broken_key(module):
|
|
||||||
if self.regenerate in ('full_idempotence', 'always'):
|
|
||||||
return False
|
|
||||||
module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.'
|
|
||||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
|
||||||
' set to `full_idempotence` or `always`, or with `force=yes`.')
|
|
||||||
|
|
||||||
proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path], check_rc=False)
|
|
||||||
if not proc[0] == 0:
|
|
||||||
if os.path.isdir(self.path):
|
|
||||||
module.fail_json(msg='%s is a directory. Please specify a path to a file.' % (self.path))
|
|
||||||
|
|
||||||
if self.regenerate in ('full_idempotence', 'always'):
|
|
||||||
return False
|
|
||||||
module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.'
|
|
||||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
|
||||||
' set to `full_idempotence` or `always`, or with `force=yes`.')
|
|
||||||
|
|
||||||
fingerprint = proc[1].split()
|
|
||||||
keysize = int(fingerprint[0])
|
|
||||||
keytype = fingerprint[-1][1:-1].lower()
|
|
||||||
|
|
||||||
self.fingerprint = fingerprint
|
|
||||||
|
|
||||||
if self.regenerate == 'never':
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _check_type():
|
|
||||||
return self.type == keytype
|
|
||||||
|
|
||||||
def _check_size():
|
|
||||||
return self.size == keysize
|
|
||||||
|
|
||||||
if not (_check_type() and _check_size()):
|
|
||||||
if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'):
|
|
||||||
return False
|
|
||||||
module.fail_json(msg='Key has wrong type and/or size.'
|
|
||||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
|
||||||
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.')
|
|
||||||
|
|
||||||
def _check_perms(module):
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
|
||||||
return not module.set_fs_attributes_if_different(file_args, False)
|
|
||||||
|
|
||||||
return not perms_required or _check_perms(module)
|
|
||||||
|
|
||||||
def isPublicKeyValid(self, module, perms_required=True):
|
|
||||||
|
|
||||||
def _get_pubkey_content():
|
|
||||||
if os.path.exists(self.path + ".pub"):
|
|
||||||
with open(self.path + ".pub", "r") as pubkey_f:
|
|
||||||
present_pubkey = pubkey_f.read().strip(' \n')
|
|
||||||
return present_pubkey
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _parse_pubkey(pubkey_content):
|
|
||||||
if pubkey_content:
|
|
||||||
parts = pubkey_content.split(' ', 2)
|
|
||||||
if len(parts) < 2:
|
|
||||||
return False
|
|
||||||
return parts[0], parts[1], '' if len(parts) <= 2 else parts[2]
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _pubkey_valid(pubkey):
|
|
||||||
if pubkey_parts and _parse_pubkey(pubkey):
|
|
||||||
return pubkey_parts[:2] == _parse_pubkey(pubkey)[:2]
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _comment_valid():
|
|
||||||
if pubkey_parts:
|
|
||||||
return pubkey_parts[2] == self.comment
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _check_perms(module):
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
|
||||||
file_args['path'] = file_args['path'] + '.pub'
|
|
||||||
return not module.set_fs_attributes_if_different(file_args, False)
|
|
||||||
|
|
||||||
pubkey_parts = _parse_pubkey(_get_pubkey_content())
|
|
||||||
|
|
||||||
pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path])
|
|
||||||
pubkey = pubkey[1].strip('\n')
|
|
||||||
if _pubkey_valid(pubkey):
|
|
||||||
self.public_key = pubkey
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.comment:
|
|
||||||
if not _comment_valid():
|
|
||||||
return False
|
|
||||||
|
|
||||||
if perms_required:
|
|
||||||
if not _check_perms(module):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
# return result as a dict
|
|
||||||
|
|
||||||
"""Serialize the object into a dictionary."""
|
|
||||||
result = {
|
|
||||||
'changed': self.changed,
|
|
||||||
'size': self.size,
|
|
||||||
'type': self.type,
|
|
||||||
'filename': self.path,
|
|
||||||
# On removal this has no value
|
|
||||||
'fingerprint': self.fingerprint[1] if self.fingerprint else '',
|
|
||||||
'public_key': self.public_key,
|
|
||||||
'comment': self.comment if self.comment else '',
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def remove(self):
|
|
||||||
"""Remove the resource from the filesystem."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.remove(self.path)
|
|
||||||
self.changed = True
|
|
||||||
except OSError as exc:
|
|
||||||
if exc.errno != errno.ENOENT:
|
|
||||||
raise KeypairError(exc)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if os.path.exists(self.path + ".pub"):
|
|
||||||
try:
|
|
||||||
os.remove(self.path + ".pub")
|
|
||||||
self.changed = True
|
|
||||||
except OSError as exc:
|
|
||||||
if exc.errno != errno.ENOENT:
|
|
||||||
raise KeypairError(exc)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
|
|
||||||
# Define Ansible Module
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
|
||||||
size=dict(type='int'),
|
|
||||||
type=dict(type='str', default='rsa', choices=['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519']),
|
|
||||||
force=dict(type='bool', default=False),
|
|
||||||
path=dict(type='path', required=True),
|
|
||||||
comment=dict(type='str'),
|
|
||||||
regenerate=dict(
|
|
||||||
type='str',
|
|
||||||
default='partial_idempotence',
|
|
||||||
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
|
|
||||||
),
|
|
||||||
),
|
|
||||||
supports_check_mode=True,
|
|
||||||
add_file_common_args=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if Path exists
|
|
||||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
|
||||||
if not os.path.isdir(base_dir):
|
|
||||||
module.fail_json(
|
|
||||||
name=base_dir,
|
|
||||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
keypair = Keypair(module)
|
|
||||||
|
|
||||||
if keypair.state == 'present':
|
|
||||||
|
|
||||||
if module.check_mode:
|
|
||||||
result = keypair.dump()
|
|
||||||
result['changed'] = keypair.force or not keypair.isPrivateKeyValid(module) or not keypair.isPublicKeyValid(module)
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
try:
|
|
||||||
keypair.generate(module)
|
|
||||||
except Exception as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
else:
|
|
||||||
|
|
||||||
if module.check_mode:
|
|
||||||
keypair.changed = os.path.exists(module.params['path'])
|
|
||||||
if keypair.changed:
|
|
||||||
keypair.fingerprint = {}
|
|
||||||
result = keypair.dump()
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
try:
|
|
||||||
keypair.remove()
|
|
||||||
except Exception as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
result = keypair.dump()
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
File diff suppressed because it is too large
Load Diff
@ -1,863 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
|
||||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
---
|
|
||||||
module: openssl_certificate_info
|
|
||||||
version_added: '2.8'
|
|
||||||
short_description: Provide information of OpenSSL X.509 certificates
|
|
||||||
description:
|
|
||||||
- This module allows one to query information on OpenSSL certificates.
|
|
||||||
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
|
|
||||||
cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
|
|
||||||
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
|
|
||||||
C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
|
|
||||||
and will be removed in Ansible 2.13.
|
|
||||||
requirements:
|
|
||||||
- PyOpenSSL >= 0.15 or cryptography >= 1.6
|
|
||||||
author:
|
|
||||||
- Felix Fontein (@felixfontein)
|
|
||||||
- Yanis Guenane (@Spredzy)
|
|
||||||
- Markus Teufelberger (@MarkusTeufelberger)
|
|
||||||
options:
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- Remote absolute path where the certificate file is loaded from.
|
|
||||||
- Either I(path) or I(content) must be specified, but not both.
|
|
||||||
type: path
|
|
||||||
content:
|
|
||||||
description:
|
|
||||||
- Content of the X.509 certificate in PEM format.
|
|
||||||
- Either I(path) or I(content) must be specified, but not both.
|
|
||||||
type: str
|
|
||||||
version_added: "2.10"
|
|
||||||
valid_at:
|
|
||||||
description:
|
|
||||||
- A dict of names mapping to time specifications. Every time specified here
|
|
||||||
will be checked whether the certificate is valid at this point. See the
|
|
||||||
C(valid_at) return value for informations on the result.
|
|
||||||
- Time can be specified either as relative time or as absolute timestamp.
|
|
||||||
- Time will always be interpreted as UTC.
|
|
||||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
|
||||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h), and ASN.1 TIME (i.e. pattern C(YYYYMMDDHHMMSSZ)).
|
|
||||||
Note that all timestamps will be treated as being in UTC.
|
|
||||||
type: dict
|
|
||||||
select_crypto_backend:
|
|
||||||
description:
|
|
||||||
- Determines which crypto backend to use.
|
|
||||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
|
||||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
|
||||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
|
||||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13.
|
|
||||||
From that point on, only the C(cryptography) backend will be available.
|
|
||||||
type: str
|
|
||||||
default: auto
|
|
||||||
choices: [ auto, cryptography, pyopenssl ]
|
|
||||||
|
|
||||||
notes:
|
|
||||||
- All timestamp values are provided in ASN.1 TIME format, i.e. following the C(YYYYMMDDHHMMSSZ) pattern.
|
|
||||||
They are all in UTC.
|
|
||||||
seealso:
|
|
||||||
- module: openssl_certificate
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Generate a Self Signed OpenSSL certificate
|
|
||||||
openssl_certificate:
|
|
||||||
path: /etc/ssl/crt/ansible.com.crt
|
|
||||||
privatekey_path: /etc/ssl/private/ansible.com.pem
|
|
||||||
csr_path: /etc/ssl/csr/ansible.com.csr
|
|
||||||
provider: selfsigned
|
|
||||||
|
|
||||||
|
|
||||||
# Get information on the certificate
|
|
||||||
|
|
||||||
- name: Get information on generated certificate
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: /etc/ssl/crt/ansible.com.crt
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- name: Dump information
|
|
||||||
debug:
|
|
||||||
var: result
|
|
||||||
|
|
||||||
|
|
||||||
# Check whether the certificate is valid or not valid at certain times, fail
|
|
||||||
# if this is not the case. The first task (openssl_certificate_info) collects
|
|
||||||
# the information, and the second task (assert) validates the result and
|
|
||||||
# makes the playbook fail in case something is not as expected.
|
|
||||||
|
|
||||||
- name: Test whether that certificate is valid tomorrow and/or in three weeks
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: /etc/ssl/crt/ansible.com.crt
|
|
||||||
valid_at:
|
|
||||||
point_1: "+1d"
|
|
||||||
point_2: "+3w"
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- name: Validate that certificate is valid tomorrow, but not in three weeks
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- result.valid_at.point_1 # valid in one day
|
|
||||||
- not result.valid_at.point_2 # not valid in three weeks
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = r'''
|
|
||||||
expired:
|
|
||||||
description: Whether the certificate is expired (i.e. C(notAfter) is in the past)
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
basic_constraints:
|
|
||||||
description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
sample: "[CA:TRUE, pathlen:1]"
|
|
||||||
basic_constraints_critical:
|
|
||||||
description: Whether the C(basic_constraints) extension is critical.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
extended_key_usage:
|
|
||||||
description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
sample: "[Biometric Info, DVCS, Time Stamping]"
|
|
||||||
extended_key_usage_critical:
|
|
||||||
description: Whether the C(extended_key_usage) extension is critical.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
extensions_by_oid:
|
|
||||||
description: Returns a dictionary for every extension OID
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
contains:
|
|
||||||
critical:
|
|
||||||
description: Whether the extension is critical.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
value:
|
|
||||||
description: The Base64 encoded value (in DER format) of the extension
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: "MAMCAQU="
|
|
||||||
sample: '{"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}'
|
|
||||||
key_usage:
|
|
||||||
description: Entries in the C(key_usage) extension, or C(none) if extension is not present.
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: "[Key Agreement, Data Encipherment]"
|
|
||||||
key_usage_critical:
|
|
||||||
description: Whether the C(key_usage) extension is critical.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
subject_alt_name:
|
|
||||||
description: Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
|
|
||||||
subject_alt_name_critical:
|
|
||||||
description: Whether the C(subject_alt_name) extension is critical.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
ocsp_must_staple:
|
|
||||||
description: C(yes) if the OCSP Must Staple extension is present, C(none) otherwise.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
ocsp_must_staple_critical:
|
|
||||||
description: Whether the C(ocsp_must_staple) extension is critical.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
issuer:
|
|
||||||
description:
|
|
||||||
- The certificate's issuer.
|
|
||||||
- Note that for repeated values, only the last one will be returned.
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
|
|
||||||
issuer_ordered:
|
|
||||||
description: The certificate's issuer as an ordered list of tuples.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: list
|
|
||||||
sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]'
|
|
||||||
version_added: "2.9"
|
|
||||||
subject:
|
|
||||||
description:
|
|
||||||
- The certificate's subject as a dictionary.
|
|
||||||
- Note that for repeated values, only the last one will be returned.
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
sample: '{"commonName": "www.example.com", "emailAddress": "test@example.com"}'
|
|
||||||
subject_ordered:
|
|
||||||
description: The certificate's subject as an ordered list of tuples.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: list
|
|
||||||
sample: '[["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]'
|
|
||||||
version_added: "2.9"
|
|
||||||
not_after:
|
|
||||||
description: C(notAfter) date as ASN.1 TIME
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: 20190413202428Z
|
|
||||||
not_before:
|
|
||||||
description: C(notBefore) date as ASN.1 TIME
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: 20190331202428Z
|
|
||||||
public_key:
|
|
||||||
description: Certificate's public key in PEM format
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
|
||||||
public_key_fingerprints:
|
|
||||||
description:
|
|
||||||
- Fingerprints of certificate's public key.
|
|
||||||
- For every hash algorithm available, the fingerprint is computed.
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
|
|
||||||
'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
|
|
||||||
signature_algorithm:
|
|
||||||
description: The signature algorithm used to sign the certificate.
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: sha256WithRSAEncryption
|
|
||||||
serial_number:
|
|
||||||
description: The certificate's serial number.
|
|
||||||
returned: success
|
|
||||||
type: int
|
|
||||||
sample: 1234
|
|
||||||
version:
|
|
||||||
description: The certificate version.
|
|
||||||
returned: success
|
|
||||||
type: int
|
|
||||||
sample: 3
|
|
||||||
valid_at:
|
|
||||||
description: For every time stamp provided in the I(valid_at) option, a
|
|
||||||
boolean whether the certificate is valid at that point in time
|
|
||||||
or not.
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
subject_key_identifier:
|
|
||||||
description:
|
|
||||||
- The certificate's subject key identifier.
|
|
||||||
- The identifier is returned in hexadecimal, with C(:) used to separate bytes.
|
|
||||||
- Is C(none) if the C(SubjectKeyIdentifier) extension is not present.
|
|
||||||
returned: success and if the pyOpenSSL backend is I(not) used
|
|
||||||
type: str
|
|
||||||
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
|
|
||||||
version_added: "2.9"
|
|
||||||
authority_key_identifier:
|
|
||||||
description:
|
|
||||||
- The certificate's authority key identifier.
|
|
||||||
- The identifier is returned in hexadecimal, with C(:) used to separate bytes.
|
|
||||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
|
||||||
returned: success and if the pyOpenSSL backend is I(not) used
|
|
||||||
type: str
|
|
||||||
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
|
|
||||||
version_added: "2.9"
|
|
||||||
authority_cert_issuer:
|
|
||||||
description:
|
|
||||||
- The certificate's authority cert issuer as a list of general names.
|
|
||||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
|
||||||
returned: success and if the pyOpenSSL backend is I(not) used
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
|
|
||||||
version_added: "2.9"
|
|
||||||
authority_cert_serial_number:
|
|
||||||
description:
|
|
||||||
- The certificate's authority cert serial number.
|
|
||||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
|
||||||
returned: success and if the pyOpenSSL backend is I(not) used
|
|
||||||
type: int
|
|
||||||
sample: '12345'
|
|
||||||
version_added: "2.9"
|
|
||||||
ocsp_uri:
|
|
||||||
description: The OCSP responder URI, if included in the certificate. Will be
|
|
||||||
C(none) if no OCSP responder URI is included.
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
version_added: "2.9"
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import binascii
|
|
||||||
import datetime
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import traceback
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
|
|
||||||
from ansible.module_utils import crypto as crypto_utils
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
from ansible.module_utils.six import string_types
|
|
||||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
|
||||||
from ansible.module_utils.compat import ipaddress as compat_ipaddress
|
|
||||||
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
|
||||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
|
||||||
|
|
||||||
PYOPENSSL_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import OpenSSL
|
|
||||||
from OpenSSL import crypto
|
|
||||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
|
||||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
|
||||||
# OpenSSL 1.1.0 or newer
|
|
||||||
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
|
|
||||||
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
|
|
||||||
else:
|
|
||||||
# OpenSSL 1.0.x or older
|
|
||||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
|
||||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
|
||||||
except ImportError:
|
|
||||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
|
||||||
PYOPENSSL_FOUND = False
|
|
||||||
else:
|
|
||||||
PYOPENSSL_FOUND = True
|
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
from cryptography import x509
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
|
|
||||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateInfo(crypto_utils.OpenSSLObject):
|
|
||||||
def __init__(self, module, backend):
|
|
||||||
super(CertificateInfo, self).__init__(
|
|
||||||
module.params['path'] or '',
|
|
||||||
'present',
|
|
||||||
False,
|
|
||||||
module.check_mode,
|
|
||||||
)
|
|
||||||
self.backend = backend
|
|
||||||
self.module = module
|
|
||||||
self.content = module.params['content']
|
|
||||||
if self.content is not None:
|
|
||||||
self.content = self.content.encode('utf-8')
|
|
||||||
|
|
||||||
self.valid_at = module.params['valid_at']
|
|
||||||
if self.valid_at:
|
|
||||||
for k, v in self.valid_at.items():
|
|
||||||
if not isinstance(v, string_types):
|
|
||||||
self.module.fail_json(
|
|
||||||
msg='The value for valid_at.{0} must be of type string (got {1})'.format(k, type(v))
|
|
||||||
)
|
|
||||||
self.valid_at[k] = crypto_utils.get_relative_time_option(v, 'valid_at.{0}'.format(k))
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
# Empty method because crypto_utils.OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
# Empty method because crypto_utils.OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_signature_algorithm(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_subject_ordered(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_issuer_ordered(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_version(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_key_usage(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_extended_key_usage(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_basic_constraints(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_ocsp_must_staple(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_subject_alt_name(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_not_before(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_not_after(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_subject_key_identifier(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_authority_key_identifier(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_serial_number(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_all_extensions(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_ocsp_uri(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_info(self):
|
|
||||||
result = dict()
|
|
||||||
self.cert = crypto_utils.load_certificate(self.path, content=self.content, backend=self.backend)
|
|
||||||
|
|
||||||
result['signature_algorithm'] = self._get_signature_algorithm()
|
|
||||||
subject = self._get_subject_ordered()
|
|
||||||
issuer = self._get_issuer_ordered()
|
|
||||||
result['subject'] = dict()
|
|
||||||
for k, v in subject:
|
|
||||||
result['subject'][k] = v
|
|
||||||
result['subject_ordered'] = subject
|
|
||||||
result['issuer'] = dict()
|
|
||||||
for k, v in issuer:
|
|
||||||
result['issuer'][k] = v
|
|
||||||
result['issuer_ordered'] = issuer
|
|
||||||
result['version'] = self._get_version()
|
|
||||||
result['key_usage'], result['key_usage_critical'] = self._get_key_usage()
|
|
||||||
result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage()
|
|
||||||
result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints()
|
|
||||||
result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple()
|
|
||||||
result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name()
|
|
||||||
|
|
||||||
not_before = self._get_not_before()
|
|
||||||
not_after = self._get_not_after()
|
|
||||||
result['not_before'] = not_before.strftime(TIMESTAMP_FORMAT)
|
|
||||||
result['not_after'] = not_after.strftime(TIMESTAMP_FORMAT)
|
|
||||||
result['expired'] = not_after < datetime.datetime.utcnow()
|
|
||||||
|
|
||||||
result['valid_at'] = dict()
|
|
||||||
if self.valid_at:
|
|
||||||
for k, v in self.valid_at.items():
|
|
||||||
result['valid_at'][k] = not_before <= v <= not_after
|
|
||||||
|
|
||||||
result['public_key'] = self._get_public_key(binary=False)
|
|
||||||
pk = self._get_public_key(binary=True)
|
|
||||||
result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict()
|
|
||||||
|
|
||||||
if self.backend != 'pyopenssl':
|
|
||||||
ski = self._get_subject_key_identifier()
|
|
||||||
if ski is not None:
|
|
||||||
ski = to_native(binascii.hexlify(ski))
|
|
||||||
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
|
|
||||||
result['subject_key_identifier'] = ski
|
|
||||||
|
|
||||||
aki, aci, acsn = self._get_authority_key_identifier()
|
|
||||||
if aki is not None:
|
|
||||||
aki = to_native(binascii.hexlify(aki))
|
|
||||||
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
|
||||||
result['authority_key_identifier'] = aki
|
|
||||||
result['authority_cert_issuer'] = aci
|
|
||||||
result['authority_cert_serial_number'] = acsn
|
|
||||||
|
|
||||||
result['serial_number'] = self._get_serial_number()
|
|
||||||
result['extensions_by_oid'] = self._get_all_extensions()
|
|
||||||
result['ocsp_uri'] = self._get_ocsp_uri()
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateInfoCryptography(CertificateInfo):
|
|
||||||
"""Validate the supplied cert, using the cryptography backend"""
|
|
||||||
def __init__(self, module):
|
|
||||||
super(CertificateInfoCryptography, self).__init__(module, 'cryptography')
|
|
||||||
|
|
||||||
def _get_signature_algorithm(self):
|
|
||||||
return crypto_utils.cryptography_oid_to_name(self.cert.signature_algorithm_oid)
|
|
||||||
|
|
||||||
def _get_subject_ordered(self):
|
|
||||||
result = []
|
|
||||||
for attribute in self.cert.subject:
|
|
||||||
result.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value])
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _get_issuer_ordered(self):
|
|
||||||
result = []
|
|
||||||
for attribute in self.cert.issuer:
|
|
||||||
result.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value])
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _get_version(self):
|
|
||||||
if self.cert.version == x509.Version.v1:
|
|
||||||
return 1
|
|
||||||
if self.cert.version == x509.Version.v3:
|
|
||||||
return 3
|
|
||||||
return "unknown"
|
|
||||||
|
|
||||||
def _get_key_usage(self):
|
|
||||||
try:
|
|
||||||
current_key_ext = self.cert.extensions.get_extension_for_class(x509.KeyUsage)
|
|
||||||
current_key_usage = current_key_ext.value
|
|
||||||
key_usage = dict(
|
|
||||||
digital_signature=current_key_usage.digital_signature,
|
|
||||||
content_commitment=current_key_usage.content_commitment,
|
|
||||||
key_encipherment=current_key_usage.key_encipherment,
|
|
||||||
data_encipherment=current_key_usage.data_encipherment,
|
|
||||||
key_agreement=current_key_usage.key_agreement,
|
|
||||||
key_cert_sign=current_key_usage.key_cert_sign,
|
|
||||||
crl_sign=current_key_usage.crl_sign,
|
|
||||||
encipher_only=False,
|
|
||||||
decipher_only=False,
|
|
||||||
)
|
|
||||||
if key_usage['key_agreement']:
|
|
||||||
key_usage.update(dict(
|
|
||||||
encipher_only=current_key_usage.encipher_only,
|
|
||||||
decipher_only=current_key_usage.decipher_only
|
|
||||||
))
|
|
||||||
|
|
||||||
key_usage_names = dict(
|
|
||||||
digital_signature='Digital Signature',
|
|
||||||
content_commitment='Non Repudiation',
|
|
||||||
key_encipherment='Key Encipherment',
|
|
||||||
data_encipherment='Data Encipherment',
|
|
||||||
key_agreement='Key Agreement',
|
|
||||||
key_cert_sign='Certificate Sign',
|
|
||||||
crl_sign='CRL Sign',
|
|
||||||
encipher_only='Encipher Only',
|
|
||||||
decipher_only='Decipher Only',
|
|
||||||
)
|
|
||||||
return sorted([
|
|
||||||
key_usage_names[name] for name, value in key_usage.items() if value
|
|
||||||
]), current_key_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_extended_key_usage(self):
|
|
||||||
try:
|
|
||||||
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
|
|
||||||
return sorted([
|
|
||||||
crypto_utils.cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value
|
|
||||||
]), ext_keyusage_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_basic_constraints(self):
|
|
||||||
try:
|
|
||||||
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.BasicConstraints)
|
|
||||||
result = []
|
|
||||||
result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE'))
|
|
||||||
if ext_keyusage_ext.value.path_length is not None:
|
|
||||||
result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length))
|
|
||||||
return sorted(result), ext_keyusage_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_ocsp_must_staple(self):
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
# This only works with cryptography >= 2.1
|
|
||||||
tlsfeature_ext = self.cert.extensions.get_extension_for_class(x509.TLSFeature)
|
|
||||||
value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
|
|
||||||
except AttributeError as dummy:
|
|
||||||
# Fallback for cryptography < 2.1
|
|
||||||
oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
|
|
||||||
tlsfeature_ext = self.cert.extensions.get_extension_for_oid(oid)
|
|
||||||
value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05"
|
|
||||||
return value, tlsfeature_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_subject_alt_name(self):
|
|
||||||
try:
|
|
||||||
san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
|
||||||
result = [crypto_utils.cryptography_decode_name(san) for san in san_ext.value]
|
|
||||||
return result, san_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_not_before(self):
|
|
||||||
return self.cert.not_valid_before
|
|
||||||
|
|
||||||
def _get_not_after(self):
|
|
||||||
return self.cert.not_valid_after
|
|
||||||
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
return self.cert.public_key().public_bytes(
|
|
||||||
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
|
||||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_subject_key_identifier(self):
|
|
||||||
try:
|
|
||||||
ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
|
||||||
return ext.value.digest
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_authority_key_identifier(self):
|
|
||||||
try:
|
|
||||||
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
|
||||||
issuer = None
|
|
||||||
if ext.value.authority_cert_issuer is not None:
|
|
||||||
issuer = [crypto_utils.cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
|
|
||||||
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
def _get_serial_number(self):
|
|
||||||
return self.cert.serial_number
|
|
||||||
|
|
||||||
def _get_all_extensions(self):
|
|
||||||
return crypto_utils.cryptography_get_extensions_from_cert(self.cert)
|
|
||||||
|
|
||||||
def _get_ocsp_uri(self):
|
|
||||||
try:
|
|
||||||
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess)
|
|
||||||
for desc in ext.value:
|
|
||||||
if desc.access_method == x509.oid.AuthorityInformationAccessOID.OCSP:
|
|
||||||
if isinstance(desc.access_location, x509.UniformResourceIdentifier):
|
|
||||||
return desc.access_location.value
|
|
||||||
except x509.ExtensionNotFound as dummy:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateInfoPyOpenSSL(CertificateInfo):
|
|
||||||
"""validate the supplied certificate."""
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(CertificateInfoPyOpenSSL, self).__init__(module, 'pyopenssl')
|
|
||||||
|
|
||||||
def _get_signature_algorithm(self):
|
|
||||||
return to_text(self.cert.get_signature_algorithm())
|
|
||||||
|
|
||||||
def __get_name(self, name):
|
|
||||||
result = []
|
|
||||||
for sub in name.get_components():
|
|
||||||
result.append([crypto_utils.pyopenssl_normalize_name(sub[0]), to_text(sub[1])])
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _get_subject_ordered(self):
|
|
||||||
return self.__get_name(self.cert.get_subject())
|
|
||||||
|
|
||||||
def _get_issuer_ordered(self):
|
|
||||||
return self.__get_name(self.cert.get_issuer())
|
|
||||||
|
|
||||||
def _get_version(self):
|
|
||||||
# Version numbers in certs are off by one:
|
|
||||||
# v1: 0, v2: 1, v3: 2 ...
|
|
||||||
return self.cert.get_version() + 1
|
|
||||||
|
|
||||||
def _get_extension(self, short_name):
|
|
||||||
for extension_idx in range(0, self.cert.get_extension_count()):
|
|
||||||
extension = self.cert.get_extension(extension_idx)
|
|
||||||
if extension.get_short_name() == short_name:
|
|
||||||
result = [
|
|
||||||
crypto_utils.pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',')
|
|
||||||
]
|
|
||||||
return sorted(result), bool(extension.get_critical())
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_key_usage(self):
|
|
||||||
return self._get_extension(b'keyUsage')
|
|
||||||
|
|
||||||
def _get_extended_key_usage(self):
|
|
||||||
return self._get_extension(b'extendedKeyUsage')
|
|
||||||
|
|
||||||
def _get_basic_constraints(self):
|
|
||||||
return self._get_extension(b'basicConstraints')
|
|
||||||
|
|
||||||
def _get_ocsp_must_staple(self):
|
|
||||||
extensions = [self.cert.get_extension(i) for i in range(0, self.cert.get_extension_count())]
|
|
||||||
oms_ext = [
|
|
||||||
ext for ext in extensions
|
|
||||||
if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE
|
|
||||||
]
|
|
||||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
|
|
||||||
# Older versions of libssl don't know about OCSP Must Staple
|
|
||||||
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
|
|
||||||
if oms_ext:
|
|
||||||
return True, bool(oms_ext[0].get_critical())
|
|
||||||
else:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _normalize_san(self, san):
|
|
||||||
if san.startswith('IP Address:'):
|
|
||||||
san = 'IP:' + san[len('IP Address:'):]
|
|
||||||
if san.startswith('IP:'):
|
|
||||||
ip = compat_ipaddress.ip_address(san[3:])
|
|
||||||
san = 'IP:{0}'.format(ip.compressed)
|
|
||||||
return san
|
|
||||||
|
|
||||||
def _get_subject_alt_name(self):
|
|
||||||
for extension_idx in range(0, self.cert.get_extension_count()):
|
|
||||||
extension = self.cert.get_extension(extension_idx)
|
|
||||||
if extension.get_short_name() == b'subjectAltName':
|
|
||||||
result = [self._normalize_san(altname.strip()) for altname in
|
|
||||||
to_text(extension, errors='surrogate_or_strict').split(', ')]
|
|
||||||
return result, bool(extension.get_critical())
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_not_before(self):
|
|
||||||
time_string = to_native(self.cert.get_notBefore())
|
|
||||||
return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
|
||||||
|
|
||||||
def _get_not_after(self):
|
|
||||||
time_string = to_native(self.cert.get_notAfter())
|
|
||||||
return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
|
||||||
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
try:
|
|
||||||
return crypto.dump_publickey(
|
|
||||||
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
|
|
||||||
self.cert.get_pubkey()
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
# pyOpenSSL < 16.0:
|
|
||||||
bio = crypto._new_mem_buf()
|
|
||||||
if binary:
|
|
||||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.cert.get_pubkey()._pkey)
|
|
||||||
else:
|
|
||||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.cert.get_pubkey()._pkey)
|
|
||||||
if rc != 1:
|
|
||||||
crypto._raise_current_error()
|
|
||||||
return crypto._bio_to_string(bio)
|
|
||||||
except AttributeError:
|
|
||||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
|
||||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
|
||||||
|
|
||||||
def _get_subject_key_identifier(self):
|
|
||||||
# Won't be implemented
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_authority_key_identifier(self):
|
|
||||||
# Won't be implemented
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
def _get_serial_number(self):
|
|
||||||
return self.cert.get_serial_number()
|
|
||||||
|
|
||||||
def _get_all_extensions(self):
|
|
||||||
return crypto_utils.pyopenssl_get_extensions_from_cert(self.cert)
|
|
||||||
|
|
||||||
def _get_ocsp_uri(self):
|
|
||||||
for i in range(self.cert.get_extension_count()):
|
|
||||||
ext = self.cert.get_extension(i)
|
|
||||||
if ext.get_short_name() == b'authorityInfoAccess':
|
|
||||||
v = str(ext)
|
|
||||||
m = re.search('^OCSP - URI:(.*)$', v, flags=re.MULTILINE)
|
|
||||||
if m:
|
|
||||||
return m.group(1)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
path=dict(type='path'),
|
|
||||||
content=dict(type='str'),
|
|
||||||
valid_at=dict(type='dict'),
|
|
||||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
|
||||||
),
|
|
||||||
required_one_of=(
|
|
||||||
['path', 'content'],
|
|
||||||
),
|
|
||||||
mutually_exclusive=(
|
|
||||||
['path', 'content'],
|
|
||||||
),
|
|
||||||
supports_check_mode=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if module.params['path'] is not None:
|
|
||||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
|
||||||
if not os.path.isdir(base_dir):
|
|
||||||
module.fail_json(
|
|
||||||
name=base_dir,
|
|
||||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
backend = module.params['select_crypto_backend']
|
|
||||||
if backend == 'auto':
|
|
||||||
# Detect what backend we can use
|
|
||||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
|
||||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
|
||||||
|
|
||||||
# If cryptography is available we'll use it
|
|
||||||
if can_use_cryptography:
|
|
||||||
backend = 'cryptography'
|
|
||||||
elif can_use_pyopenssl:
|
|
||||||
backend = 'pyopenssl'
|
|
||||||
|
|
||||||
# Fail if no backend has been found
|
|
||||||
if backend == 'auto':
|
|
||||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
|
||||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
|
||||||
MINIMAL_PYOPENSSL_VERSION))
|
|
||||||
|
|
||||||
if backend == 'pyopenssl':
|
|
||||||
if not PYOPENSSL_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
|
||||||
exception=PYOPENSSL_IMP_ERR)
|
|
||||||
try:
|
|
||||||
getattr(crypto.X509Req, 'get_extensions')
|
|
||||||
except AttributeError:
|
|
||||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15')
|
|
||||||
|
|
||||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13')
|
|
||||||
certificate = CertificateInfoPyOpenSSL(module)
|
|
||||||
elif backend == 'cryptography':
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
certificate = CertificateInfoCryptography(module)
|
|
||||||
|
|
||||||
result = certificate.get_info()
|
|
||||||
module.exit_json(**result)
|
|
||||||
except crypto_utils.OpenSSLObjectError as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
File diff suppressed because it is too large
Load Diff
@ -1,667 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
|
||||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
---
|
|
||||||
module: openssl_csr_info
|
|
||||||
version_added: '2.8'
|
|
||||||
short_description: Provide information of OpenSSL Certificate Signing Requests (CSR)
|
|
||||||
description:
|
|
||||||
- This module allows one to query information on OpenSSL Certificate Signing Requests (CSR).
|
|
||||||
- In case the CSR signature cannot be validated, the module will fail. In this case, all return
|
|
||||||
variables are still returned.
|
|
||||||
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
|
|
||||||
cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
|
|
||||||
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
|
|
||||||
C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
|
|
||||||
and will be removed in Ansible 2.13.
|
|
||||||
requirements:
|
|
||||||
- PyOpenSSL >= 0.15 or cryptography >= 1.3
|
|
||||||
author:
|
|
||||||
- Felix Fontein (@felixfontein)
|
|
||||||
- Yanis Guenane (@Spredzy)
|
|
||||||
options:
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- Remote absolute path where the CSR file is loaded from.
|
|
||||||
- Either I(path) or I(content) must be specified, but not both.
|
|
||||||
type: path
|
|
||||||
content:
|
|
||||||
description:
|
|
||||||
- Content of the CSR file.
|
|
||||||
- Either I(path) or I(content) must be specified, but not both.
|
|
||||||
type: str
|
|
||||||
version_added: "2.10"
|
|
||||||
select_crypto_backend:
|
|
||||||
description:
|
|
||||||
- Determines which crypto backend to use.
|
|
||||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
|
||||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
|
||||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
|
||||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13.
|
|
||||||
From that point on, only the C(cryptography) backend will be available.
|
|
||||||
type: str
|
|
||||||
default: auto
|
|
||||||
choices: [ auto, cryptography, pyopenssl ]
|
|
||||||
|
|
||||||
seealso:
|
|
||||||
- module: openssl_csr
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Generate an OpenSSL Certificate Signing Request
|
|
||||||
openssl_csr:
|
|
||||||
path: /etc/ssl/csr/www.ansible.com.csr
|
|
||||||
privatekey_path: /etc/ssl/private/ansible.com.pem
|
|
||||||
common_name: www.ansible.com
|
|
||||||
|
|
||||||
- name: Get information on the CSR
|
|
||||||
openssl_csr_info:
|
|
||||||
path: /etc/ssl/csr/www.ansible.com.csr
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- name: Dump information
|
|
||||||
debug:
|
|
||||||
var: result
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = r'''
|
|
||||||
signature_valid:
|
|
||||||
description:
|
|
||||||
- Whether the CSR's signature is valid.
|
|
||||||
- In case the check returns C(no), the module will fail.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
basic_constraints:
|
|
||||||
description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
sample: "[CA:TRUE, pathlen:1]"
|
|
||||||
basic_constraints_critical:
|
|
||||||
description: Whether the C(basic_constraints) extension is critical.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
extended_key_usage:
|
|
||||||
description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
sample: "[Biometric Info, DVCS, Time Stamping]"
|
|
||||||
extended_key_usage_critical:
|
|
||||||
description: Whether the C(extended_key_usage) extension is critical.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
extensions_by_oid:
|
|
||||||
description: Returns a dictionary for every extension OID
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
contains:
|
|
||||||
critical:
|
|
||||||
description: Whether the extension is critical.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
value:
|
|
||||||
description: The Base64 encoded value (in DER format) of the extension
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: "MAMCAQU="
|
|
||||||
sample: '{"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}'
|
|
||||||
key_usage:
|
|
||||||
description: Entries in the C(key_usage) extension, or C(none) if extension is not present.
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: "[Key Agreement, Data Encipherment]"
|
|
||||||
key_usage_critical:
|
|
||||||
description: Whether the C(key_usage) extension is critical.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
subject_alt_name:
|
|
||||||
description: Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
|
|
||||||
subject_alt_name_critical:
|
|
||||||
description: Whether the C(subject_alt_name) extension is critical.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
ocsp_must_staple:
|
|
||||||
description: C(yes) if the OCSP Must Staple extension is present, C(none) otherwise.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
ocsp_must_staple_critical:
|
|
||||||
description: Whether the C(ocsp_must_staple) extension is critical.
|
|
||||||
returned: success
|
|
||||||
type: bool
|
|
||||||
subject:
|
|
||||||
description:
|
|
||||||
- The CSR's subject as a dictionary.
|
|
||||||
- Note that for repeated values, only the last one will be returned.
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
sample: '{"commonName": "www.example.com", "emailAddress": "test@example.com"}'
|
|
||||||
subject_ordered:
|
|
||||||
description: The CSR's subject as an ordered list of tuples.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: list
|
|
||||||
sample: '[["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]'
|
|
||||||
version_added: "2.9"
|
|
||||||
public_key:
|
|
||||||
description: CSR's public key in PEM format
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
|
||||||
public_key_fingerprints:
|
|
||||||
description:
|
|
||||||
- Fingerprints of CSR's public key.
|
|
||||||
- For every hash algorithm available, the fingerprint is computed.
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
|
|
||||||
'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
|
|
||||||
subject_key_identifier:
|
|
||||||
description:
|
|
||||||
- The CSR's subject key identifier.
|
|
||||||
- The identifier is returned in hexadecimal, with C(:) used to separate bytes.
|
|
||||||
- Is C(none) if the C(SubjectKeyIdentifier) extension is not present.
|
|
||||||
returned: success and if the pyOpenSSL backend is I(not) used
|
|
||||||
type: str
|
|
||||||
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
|
|
||||||
version_added: "2.9"
|
|
||||||
authority_key_identifier:
|
|
||||||
description:
|
|
||||||
- The CSR's authority key identifier.
|
|
||||||
- The identifier is returned in hexadecimal, with C(:) used to separate bytes.
|
|
||||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
|
||||||
returned: success and if the pyOpenSSL backend is I(not) used
|
|
||||||
type: str
|
|
||||||
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
|
|
||||||
version_added: "2.9"
|
|
||||||
authority_cert_issuer:
|
|
||||||
description:
|
|
||||||
- The CSR's authority cert issuer as a list of general names.
|
|
||||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
|
||||||
returned: success and if the pyOpenSSL backend is I(not) used
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
|
|
||||||
version_added: "2.9"
|
|
||||||
authority_cert_serial_number:
|
|
||||||
description:
|
|
||||||
- The CSR's authority cert serial number.
|
|
||||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
|
||||||
returned: success and if the pyOpenSSL backend is I(not) used
|
|
||||||
type: int
|
|
||||||
sample: '12345'
|
|
||||||
version_added: "2.9"
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import binascii
|
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
|
|
||||||
from ansible.module_utils import crypto as crypto_utils
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
|
||||||
from ansible.module_utils.compat import ipaddress as compat_ipaddress
|
|
||||||
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
|
|
||||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
|
||||||
|
|
||||||
PYOPENSSL_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import OpenSSL
|
|
||||||
from OpenSSL import crypto
|
|
||||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
|
||||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
|
||||||
# OpenSSL 1.1.0 or newer
|
|
||||||
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
|
|
||||||
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
|
|
||||||
else:
|
|
||||||
# OpenSSL 1.0.x or older
|
|
||||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
|
||||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
|
||||||
except ImportError:
|
|
||||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
|
||||||
PYOPENSSL_FOUND = False
|
|
||||||
else:
|
|
||||||
PYOPENSSL_FOUND = True
|
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
from cryptography import x509
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
|
|
||||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateSigningRequestInfo(crypto_utils.OpenSSLObject):
|
|
||||||
def __init__(self, module, backend):
|
|
||||||
super(CertificateSigningRequestInfo, self).__init__(
|
|
||||||
module.params['path'] or '',
|
|
||||||
'present',
|
|
||||||
False,
|
|
||||||
module.check_mode,
|
|
||||||
)
|
|
||||||
self.backend = backend
|
|
||||||
self.module = module
|
|
||||||
self.content = module.params['content']
|
|
||||||
if self.content is not None:
|
|
||||||
self.content = self.content.encode('utf-8')
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
# Empty method because crypto_utils.OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
# Empty method because crypto_utils.OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_subject_ordered(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_key_usage(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_extended_key_usage(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_basic_constraints(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_ocsp_must_staple(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_subject_alt_name(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_subject_key_identifier(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_authority_key_identifier(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_all_extensions(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _is_signature_valid(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_info(self):
|
|
||||||
result = dict()
|
|
||||||
self.csr = crypto_utils.load_certificate_request(self.path, content=self.content, backend=self.backend)
|
|
||||||
|
|
||||||
subject = self._get_subject_ordered()
|
|
||||||
result['subject'] = dict()
|
|
||||||
for k, v in subject:
|
|
||||||
result['subject'][k] = v
|
|
||||||
result['subject_ordered'] = subject
|
|
||||||
result['key_usage'], result['key_usage_critical'] = self._get_key_usage()
|
|
||||||
result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage()
|
|
||||||
result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints()
|
|
||||||
result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple()
|
|
||||||
result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name()
|
|
||||||
|
|
||||||
result['public_key'] = self._get_public_key(binary=False)
|
|
||||||
pk = self._get_public_key(binary=True)
|
|
||||||
result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict()
|
|
||||||
|
|
||||||
if self.backend != 'pyopenssl':
|
|
||||||
ski = self._get_subject_key_identifier()
|
|
||||||
if ski is not None:
|
|
||||||
ski = to_native(binascii.hexlify(ski))
|
|
||||||
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
|
|
||||||
result['subject_key_identifier'] = ski
|
|
||||||
|
|
||||||
aki, aci, acsn = self._get_authority_key_identifier()
|
|
||||||
if aki is not None:
|
|
||||||
aki = to_native(binascii.hexlify(aki))
|
|
||||||
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
|
||||||
result['authority_key_identifier'] = aki
|
|
||||||
result['authority_cert_issuer'] = aci
|
|
||||||
result['authority_cert_serial_number'] = acsn
|
|
||||||
|
|
||||||
result['extensions_by_oid'] = self._get_all_extensions()
|
|
||||||
|
|
||||||
result['signature_valid'] = self._is_signature_valid()
|
|
||||||
if not result['signature_valid']:
|
|
||||||
self.module.fail_json(
|
|
||||||
msg='CSR signature is invalid!',
|
|
||||||
**result
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateSigningRequestInfoCryptography(CertificateSigningRequestInfo):
|
|
||||||
"""Validate the supplied CSR, using the cryptography backend"""
|
|
||||||
def __init__(self, module):
|
|
||||||
super(CertificateSigningRequestInfoCryptography, self).__init__(module, 'cryptography')
|
|
||||||
|
|
||||||
def _get_subject_ordered(self):
|
|
||||||
result = []
|
|
||||||
for attribute in self.csr.subject:
|
|
||||||
result.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value])
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _get_key_usage(self):
|
|
||||||
try:
|
|
||||||
current_key_ext = self.csr.extensions.get_extension_for_class(x509.KeyUsage)
|
|
||||||
current_key_usage = current_key_ext.value
|
|
||||||
key_usage = dict(
|
|
||||||
digital_signature=current_key_usage.digital_signature,
|
|
||||||
content_commitment=current_key_usage.content_commitment,
|
|
||||||
key_encipherment=current_key_usage.key_encipherment,
|
|
||||||
data_encipherment=current_key_usage.data_encipherment,
|
|
||||||
key_agreement=current_key_usage.key_agreement,
|
|
||||||
key_cert_sign=current_key_usage.key_cert_sign,
|
|
||||||
crl_sign=current_key_usage.crl_sign,
|
|
||||||
encipher_only=False,
|
|
||||||
decipher_only=False,
|
|
||||||
)
|
|
||||||
if key_usage['key_agreement']:
|
|
||||||
key_usage.update(dict(
|
|
||||||
encipher_only=current_key_usage.encipher_only,
|
|
||||||
decipher_only=current_key_usage.decipher_only
|
|
||||||
))
|
|
||||||
|
|
||||||
key_usage_names = dict(
|
|
||||||
digital_signature='Digital Signature',
|
|
||||||
content_commitment='Non Repudiation',
|
|
||||||
key_encipherment='Key Encipherment',
|
|
||||||
data_encipherment='Data Encipherment',
|
|
||||||
key_agreement='Key Agreement',
|
|
||||||
key_cert_sign='Certificate Sign',
|
|
||||||
crl_sign='CRL Sign',
|
|
||||||
encipher_only='Encipher Only',
|
|
||||||
decipher_only='Decipher Only',
|
|
||||||
)
|
|
||||||
return sorted([
|
|
||||||
key_usage_names[name] for name, value in key_usage.items() if value
|
|
||||||
]), current_key_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_extended_key_usage(self):
|
|
||||||
try:
|
|
||||||
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
|
|
||||||
return sorted([
|
|
||||||
crypto_utils.cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value
|
|
||||||
]), ext_keyusage_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_basic_constraints(self):
|
|
||||||
try:
|
|
||||||
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.BasicConstraints)
|
|
||||||
result = []
|
|
||||||
result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE'))
|
|
||||||
if ext_keyusage_ext.value.path_length is not None:
|
|
||||||
result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length))
|
|
||||||
return sorted(result), ext_keyusage_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_ocsp_must_staple(self):
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
# This only works with cryptography >= 2.1
|
|
||||||
tlsfeature_ext = self.csr.extensions.get_extension_for_class(x509.TLSFeature)
|
|
||||||
value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
|
|
||||||
except AttributeError as dummy:
|
|
||||||
# Fallback for cryptography < 2.1
|
|
||||||
oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
|
|
||||||
tlsfeature_ext = self.csr.extensions.get_extension_for_oid(oid)
|
|
||||||
value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05"
|
|
||||||
return value, tlsfeature_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_subject_alt_name(self):
|
|
||||||
try:
|
|
||||||
san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
|
||||||
result = [crypto_utils.cryptography_decode_name(san) for san in san_ext.value]
|
|
||||||
return result, san_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
return self.csr.public_key().public_bytes(
|
|
||||||
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
|
||||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_subject_key_identifier(self):
|
|
||||||
try:
|
|
||||||
ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
|
||||||
return ext.value.digest
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_authority_key_identifier(self):
|
|
||||||
try:
|
|
||||||
ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
|
||||||
issuer = None
|
|
||||||
if ext.value.authority_cert_issuer is not None:
|
|
||||||
issuer = [crypto_utils.cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
|
|
||||||
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
def _get_all_extensions(self):
|
|
||||||
return crypto_utils.cryptography_get_extensions_from_csr(self.csr)
|
|
||||||
|
|
||||||
def _is_signature_valid(self):
|
|
||||||
return self.csr.is_signature_valid
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateSigningRequestInfoPyOpenSSL(CertificateSigningRequestInfo):
|
|
||||||
"""validate the supplied CSR."""
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(CertificateSigningRequestInfoPyOpenSSL, self).__init__(module, 'pyopenssl')
|
|
||||||
|
|
||||||
def __get_name(self, name):
|
|
||||||
result = []
|
|
||||||
for sub in name.get_components():
|
|
||||||
result.append([crypto_utils.pyopenssl_normalize_name(sub[0]), to_text(sub[1])])
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _get_subject_ordered(self):
|
|
||||||
return self.__get_name(self.csr.get_subject())
|
|
||||||
|
|
||||||
def _get_extension(self, short_name):
|
|
||||||
for extension in self.csr.get_extensions():
|
|
||||||
if extension.get_short_name() == short_name:
|
|
||||||
result = [
|
|
||||||
crypto_utils.pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',')
|
|
||||||
]
|
|
||||||
return sorted(result), bool(extension.get_critical())
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_key_usage(self):
|
|
||||||
return self._get_extension(b'keyUsage')
|
|
||||||
|
|
||||||
def _get_extended_key_usage(self):
|
|
||||||
return self._get_extension(b'extendedKeyUsage')
|
|
||||||
|
|
||||||
def _get_basic_constraints(self):
|
|
||||||
return self._get_extension(b'basicConstraints')
|
|
||||||
|
|
||||||
def _get_ocsp_must_staple(self):
|
|
||||||
extensions = self.csr.get_extensions()
|
|
||||||
oms_ext = [
|
|
||||||
ext for ext in extensions
|
|
||||||
if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE
|
|
||||||
]
|
|
||||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
|
|
||||||
# Older versions of libssl don't know about OCSP Must Staple
|
|
||||||
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
|
|
||||||
if oms_ext:
|
|
||||||
return True, bool(oms_ext[0].get_critical())
|
|
||||||
else:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _normalize_san(self, san):
|
|
||||||
# apparently openssl returns 'IP address' not 'IP' as specifier when converting the subjectAltName to string
|
|
||||||
# although it won't accept this specifier when generating the CSR. (https://github.com/openssl/openssl/issues/4004)
|
|
||||||
if san.startswith('IP Address:'):
|
|
||||||
san = 'IP:' + san[len('IP Address:'):]
|
|
||||||
if san.startswith('IP:'):
|
|
||||||
ip = compat_ipaddress.ip_address(san[3:])
|
|
||||||
san = 'IP:{0}'.format(ip.compressed)
|
|
||||||
return san
|
|
||||||
|
|
||||||
def _get_subject_alt_name(self):
|
|
||||||
for extension in self.csr.get_extensions():
|
|
||||||
if extension.get_short_name() == b'subjectAltName':
|
|
||||||
result = [self._normalize_san(altname.strip()) for altname in
|
|
||||||
to_text(extension, errors='surrogate_or_strict').split(', ')]
|
|
||||||
return result, bool(extension.get_critical())
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
try:
|
|
||||||
return crypto.dump_publickey(
|
|
||||||
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
|
|
||||||
self.csr.get_pubkey()
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
bio = crypto._new_mem_buf()
|
|
||||||
if binary:
|
|
||||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.csr.get_pubkey()._pkey)
|
|
||||||
else:
|
|
||||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.csr.get_pubkey()._pkey)
|
|
||||||
if rc != 1:
|
|
||||||
crypto._raise_current_error()
|
|
||||||
return crypto._bio_to_string(bio)
|
|
||||||
except AttributeError:
|
|
||||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
|
||||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
|
||||||
|
|
||||||
def _get_subject_key_identifier(self):
|
|
||||||
# Won't be implemented
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_authority_key_identifier(self):
|
|
||||||
# Won't be implemented
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
def _get_all_extensions(self):
|
|
||||||
return crypto_utils.pyopenssl_get_extensions_from_csr(self.csr)
|
|
||||||
|
|
||||||
def _is_signature_valid(self):
|
|
||||||
try:
|
|
||||||
return bool(self.csr.verify(self.csr.get_pubkey()))
|
|
||||||
except crypto.Error:
|
|
||||||
# OpenSSL error means that key is not consistent
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
path=dict(type='path'),
|
|
||||||
content=dict(type='str'),
|
|
||||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
|
||||||
),
|
|
||||||
required_one_of=(
|
|
||||||
['path', 'content'],
|
|
||||||
),
|
|
||||||
mutually_exclusive=(
|
|
||||||
['path', 'content'],
|
|
||||||
),
|
|
||||||
supports_check_mode=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if module.params['path'] is not None:
|
|
||||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
|
||||||
if not os.path.isdir(base_dir):
|
|
||||||
module.fail_json(
|
|
||||||
name=base_dir,
|
|
||||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
backend = module.params['select_crypto_backend']
|
|
||||||
if backend == 'auto':
|
|
||||||
# Detect what backend we can use
|
|
||||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
|
||||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
|
||||||
|
|
||||||
# If cryptography is available we'll use it
|
|
||||||
if can_use_cryptography:
|
|
||||||
backend = 'cryptography'
|
|
||||||
elif can_use_pyopenssl:
|
|
||||||
backend = 'pyopenssl'
|
|
||||||
|
|
||||||
# Fail if no backend has been found
|
|
||||||
if backend == 'auto':
|
|
||||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
|
||||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
|
||||||
MINIMAL_PYOPENSSL_VERSION))
|
|
||||||
|
|
||||||
if backend == 'pyopenssl':
|
|
||||||
if not PYOPENSSL_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
|
||||||
exception=PYOPENSSL_IMP_ERR)
|
|
||||||
try:
|
|
||||||
getattr(crypto.X509Req, 'get_extensions')
|
|
||||||
except AttributeError:
|
|
||||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15')
|
|
||||||
|
|
||||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13')
|
|
||||||
certificate = CertificateSigningRequestInfoPyOpenSSL(module)
|
|
||||||
elif backend == 'cryptography':
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
certificate = CertificateSigningRequestInfoCryptography(module)
|
|
||||||
|
|
||||||
result = certificate.get_info()
|
|
||||||
module.exit_json(**result)
|
|
||||||
except crypto_utils.OpenSSLObjectError as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
@ -1,418 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright: (c) 2017, Thom Wiggers <ansible@thomwiggers.nl>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
---
|
|
||||||
module: openssl_dhparam
|
|
||||||
version_added: "2.5"
|
|
||||||
short_description: Generate OpenSSL Diffie-Hellman Parameters
|
|
||||||
description:
|
|
||||||
- This module allows one to (re)generate OpenSSL DH-params.
|
|
||||||
- This module uses file common arguments to specify generated file permissions.
|
|
||||||
- "Please note that the module regenerates existing DH params if they don't
|
|
||||||
match the module's options. If you are concerned that this could overwrite
|
|
||||||
your existing DH params, consider using the I(backup) option."
|
|
||||||
- The module can use the cryptography Python library, or the C(openssl) executable.
|
|
||||||
By default, it tries to detect which one is available. This can be overridden
|
|
||||||
with the I(select_crypto_backend) option.
|
|
||||||
requirements:
|
|
||||||
- Either cryptography >= 2.0
|
|
||||||
- Or OpenSSL binary C(openssl)
|
|
||||||
author:
|
|
||||||
- Thom Wiggers (@thomwiggers)
|
|
||||||
options:
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- Whether the parameters should exist or not,
|
|
||||||
taking action if the state is different from what is stated.
|
|
||||||
type: str
|
|
||||||
default: present
|
|
||||||
choices: [ absent, present ]
|
|
||||||
size:
|
|
||||||
description:
|
|
||||||
- Size (in bits) of the generated DH-params.
|
|
||||||
type: int
|
|
||||||
default: 4096
|
|
||||||
force:
|
|
||||||
description:
|
|
||||||
- Should the parameters be regenerated even it it already exists.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- Name of the file in which the generated parameters will be saved.
|
|
||||||
type: path
|
|
||||||
required: true
|
|
||||||
backup:
|
|
||||||
description:
|
|
||||||
- Create a backup file including a timestamp so you can get the original
|
|
||||||
DH params back if you overwrote them with new ones by accident.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
version_added: "2.8"
|
|
||||||
select_crypto_backend:
|
|
||||||
description:
|
|
||||||
- Determines which crypto backend to use.
|
|
||||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(openssl).
|
|
||||||
- If set to C(openssl), will try to use the OpenSSL C(openssl) executable.
|
|
||||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
|
||||||
type: str
|
|
||||||
default: auto
|
|
||||||
choices: [ auto, cryptography, openssl ]
|
|
||||||
version_added: '2.10'
|
|
||||||
return_content:
|
|
||||||
description:
|
|
||||||
- If set to C(yes), will return the (current or generated) DH params' content as I(dhparams).
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
version_added: "2.10"
|
|
||||||
extends_documentation_fragment:
|
|
||||||
- files
|
|
||||||
seealso:
|
|
||||||
- module: openssl_certificate
|
|
||||||
- module: openssl_csr
|
|
||||||
- module: openssl_pkcs12
|
|
||||||
- module: openssl_privatekey
|
|
||||||
- module: openssl_publickey
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Generate Diffie-Hellman parameters with the default size (4096 bits)
|
|
||||||
openssl_dhparam:
|
|
||||||
path: /etc/ssl/dhparams.pem
|
|
||||||
|
|
||||||
- name: Generate DH Parameters with a different size (2048 bits)
|
|
||||||
openssl_dhparam:
|
|
||||||
path: /etc/ssl/dhparams.pem
|
|
||||||
size: 2048
|
|
||||||
|
|
||||||
- name: Force regenerate an DH parameters if they already exist
|
|
||||||
openssl_dhparam:
|
|
||||||
path: /etc/ssl/dhparams.pem
|
|
||||||
force: yes
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = r'''
|
|
||||||
size:
|
|
||||||
description: Size (in bits) of the Diffie-Hellman parameters.
|
|
||||||
returned: changed or success
|
|
||||||
type: int
|
|
||||||
sample: 4096
|
|
||||||
filename:
|
|
||||||
description: Path to the generated Diffie-Hellman parameters.
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: /etc/ssl/dhparams.pem
|
|
||||||
backup_file:
|
|
||||||
description: Name of backup file created.
|
|
||||||
returned: changed and if I(backup) is C(yes)
|
|
||||||
type: str
|
|
||||||
sample: /path/to/dhparams.pem.2019-03-09@11:22~
|
|
||||||
dhparams:
|
|
||||||
description: The (current or generated) DH params' content.
|
|
||||||
returned: if I(state) is C(present) and I(return_content) is C(yes)
|
|
||||||
type: str
|
|
||||||
version_added: "2.10"
|
|
||||||
'''
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import tempfile
|
|
||||||
import traceback
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
from ansible.module_utils._text import to_native
|
|
||||||
from ansible.module_utils import crypto as crypto_utils
|
|
||||||
|
|
||||||
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '2.0'
|
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
import cryptography.exceptions
|
|
||||||
import cryptography.hazmat.backends
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.dh
|
|
||||||
import cryptography.hazmat.primitives.serialization
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
|
|
||||||
class DHParameterError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DHParameterBase(object):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
self.state = module.params['state']
|
|
||||||
self.path = module.params['path']
|
|
||||||
self.size = module.params['size']
|
|
||||||
self.force = module.params['force']
|
|
||||||
self.changed = False
|
|
||||||
self.return_content = module.params['return_content']
|
|
||||||
|
|
||||||
self.backup = module.params['backup']
|
|
||||||
self.backup_file = None
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _do_generate(self, module):
|
|
||||||
"""Actually generate the DH params."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def generate(self, module):
|
|
||||||
"""Generate DH params."""
|
|
||||||
changed = False
|
|
||||||
|
|
||||||
# ony generate when necessary
|
|
||||||
if self.force or not self._check_params_valid(module):
|
|
||||||
self._do_generate(module)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# fix permissions (checking force not necessary as done above)
|
|
||||||
if not self._check_fs_attributes(module):
|
|
||||||
# Fix done implicitly by
|
|
||||||
# AnsibleModule.set_fs_attributes_if_different
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
self.changed = changed
|
|
||||||
|
|
||||||
def remove(self, module):
|
|
||||||
if self.backup:
|
|
||||||
self.backup_file = module.backup_local(self.path)
|
|
||||||
try:
|
|
||||||
os.remove(self.path)
|
|
||||||
self.changed = True
|
|
||||||
except OSError as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
def check(self, module):
|
|
||||||
"""Ensure the resource is in its desired state."""
|
|
||||||
if self.force:
|
|
||||||
return False
|
|
||||||
return self._check_params_valid(module) and self._check_fs_attributes(module)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _check_params_valid(self, module):
|
|
||||||
"""Check if the params are in the correct state"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _check_fs_attributes(self, module):
|
|
||||||
"""Checks (and changes if not in check mode!) fs attributes"""
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
|
||||||
attrs_changed = module.set_fs_attributes_if_different(file_args, False)
|
|
||||||
|
|
||||||
return not attrs_changed
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
"""Serialize the object into a dictionary."""
|
|
||||||
|
|
||||||
result = {
|
|
||||||
'size': self.size,
|
|
||||||
'filename': self.path,
|
|
||||||
'changed': self.changed,
|
|
||||||
}
|
|
||||||
if self.backup_file:
|
|
||||||
result['backup_file'] = self.backup_file
|
|
||||||
if self.return_content:
|
|
||||||
content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True)
|
|
||||||
result['dhparams'] = content.decode('utf-8') if content else None
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class DHParameterAbsent(DHParameterBase):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(DHParameterAbsent, self).__init__(module)
|
|
||||||
|
|
||||||
def _do_generate(self, module):
|
|
||||||
"""Actually generate the DH params."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _check_params_valid(self, module):
|
|
||||||
"""Check if the params are in the correct state"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DHParameterOpenSSL(DHParameterBase):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(DHParameterOpenSSL, self).__init__(module)
|
|
||||||
self.openssl_bin = module.get_bin_path('openssl', True)
|
|
||||||
|
|
||||||
def _do_generate(self, module):
|
|
||||||
"""Actually generate the DH params."""
|
|
||||||
# create a tempfile
|
|
||||||
fd, tmpsrc = tempfile.mkstemp()
|
|
||||||
os.close(fd)
|
|
||||||
module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
|
|
||||||
# openssl dhparam -out <path> <bits>
|
|
||||||
command = [self.openssl_bin, 'dhparam', '-out', tmpsrc, str(self.size)]
|
|
||||||
rc, dummy, err = module.run_command(command, check_rc=False)
|
|
||||||
if rc != 0:
|
|
||||||
raise DHParameterError(to_native(err))
|
|
||||||
if self.backup:
|
|
||||||
self.backup_file = module.backup_local(self.path)
|
|
||||||
try:
|
|
||||||
module.atomic_move(tmpsrc, self.path)
|
|
||||||
except Exception as e:
|
|
||||||
module.fail_json(msg="Failed to write to file %s: %s" % (self.path, str(e)))
|
|
||||||
|
|
||||||
def _check_params_valid(self, module):
|
|
||||||
"""Check if the params are in the correct state"""
|
|
||||||
command = [self.openssl_bin, 'dhparam', '-check', '-text', '-noout', '-in', self.path]
|
|
||||||
rc, out, err = module.run_command(command, check_rc=False)
|
|
||||||
result = to_native(out)
|
|
||||||
if rc != 0:
|
|
||||||
# If the call failed the file probably doesn't exist or is
|
|
||||||
# unreadable
|
|
||||||
return False
|
|
||||||
# output contains "(xxxx bit)"
|
|
||||||
match = re.search(r"Parameters:\s+\((\d+) bit\).*", result)
|
|
||||||
if not match:
|
|
||||||
return False # No "xxxx bit" in output
|
|
||||||
|
|
||||||
bits = int(match.group(1))
|
|
||||||
|
|
||||||
# if output contains "WARNING" we've got a problem
|
|
||||||
if "WARNING" in result or "WARNING" in to_native(err):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return bits == self.size
|
|
||||||
|
|
||||||
|
|
||||||
class DHParameterCryptography(DHParameterBase):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(DHParameterCryptography, self).__init__(module)
|
|
||||||
self.crypto_backend = cryptography.hazmat.backends.default_backend()
|
|
||||||
|
|
||||||
def _do_generate(self, module):
|
|
||||||
"""Actually generate the DH params."""
|
|
||||||
# Generate parameters
|
|
||||||
params = cryptography.hazmat.primitives.asymmetric.dh.generate_parameters(
|
|
||||||
generator=2,
|
|
||||||
key_size=self.size,
|
|
||||||
backend=self.crypto_backend,
|
|
||||||
)
|
|
||||||
# Serialize parameters
|
|
||||||
result = params.parameter_bytes(
|
|
||||||
encoding=cryptography.hazmat.primitives.serialization.Encoding.PEM,
|
|
||||||
format=cryptography.hazmat.primitives.serialization.ParameterFormat.PKCS3,
|
|
||||||
)
|
|
||||||
# Write result
|
|
||||||
if self.backup:
|
|
||||||
self.backup_file = module.backup_local(self.path)
|
|
||||||
crypto_utils.write_file(module, result)
|
|
||||||
|
|
||||||
def _check_params_valid(self, module):
|
|
||||||
"""Check if the params are in the correct state"""
|
|
||||||
# Load parameters
|
|
||||||
try:
|
|
||||||
with open(self.path, 'rb') as f:
|
|
||||||
data = f.read()
|
|
||||||
params = self.crypto_backend.load_pem_parameters(data)
|
|
||||||
except Exception as dummy:
|
|
||||||
return False
|
|
||||||
# Check parameters
|
|
||||||
bits = crypto_utils.count_bits(params.parameter_numbers().p)
|
|
||||||
return bits == self.size
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main function"""
|
|
||||||
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
|
||||||
size=dict(type='int', default=4096),
|
|
||||||
force=dict(type='bool', default=False),
|
|
||||||
path=dict(type='path', required=True),
|
|
||||||
backup=dict(type='bool', default=False),
|
|
||||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'openssl']),
|
|
||||||
return_content=dict(type='bool', default=False),
|
|
||||||
),
|
|
||||||
supports_check_mode=True,
|
|
||||||
add_file_common_args=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
|
||||||
if not os.path.isdir(base_dir):
|
|
||||||
module.fail_json(
|
|
||||||
name=base_dir,
|
|
||||||
msg="The directory '%s' does not exist or the file is not a directory" % base_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
if module.params['state'] == 'present':
|
|
||||||
backend = module.params['select_crypto_backend']
|
|
||||||
if backend == 'auto':
|
|
||||||
# Detection what is possible
|
|
||||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
|
||||||
can_use_openssl = module.get_bin_path('openssl', False) is not None
|
|
||||||
|
|
||||||
# First try cryptography, then OpenSSL
|
|
||||||
if can_use_cryptography:
|
|
||||||
backend = 'cryptography'
|
|
||||||
elif can_use_openssl:
|
|
||||||
backend = 'openssl'
|
|
||||||
|
|
||||||
# Success?
|
|
||||||
if backend == 'auto':
|
|
||||||
module.fail_json(msg=("Can't detect either the required Python library cryptography (>= {0}) "
|
|
||||||
"or the OpenSSL binary openssl").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
|
||||||
|
|
||||||
if backend == 'openssl':
|
|
||||||
dhparam = DHParameterOpenSSL(module)
|
|
||||||
elif backend == 'cryptography':
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
dhparam = DHParameterCryptography(module)
|
|
||||||
|
|
||||||
if module.check_mode:
|
|
||||||
result = dhparam.dump()
|
|
||||||
result['changed'] = module.params['force'] or not dhparam.check(module)
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
try:
|
|
||||||
dhparam.generate(module)
|
|
||||||
except DHParameterError as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
else:
|
|
||||||
dhparam = DHParameterAbsent(module)
|
|
||||||
|
|
||||||
if module.check_mode:
|
|
||||||
result = dhparam.dump()
|
|
||||||
result['changed'] = os.path.exists(module.params['path'])
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
if os.path.exists(module.params['path']):
|
|
||||||
try:
|
|
||||||
dhparam.remove(module)
|
|
||||||
except Exception as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
result = dhparam.dump()
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,470 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright: (c) 2017, Guillaume Delpierre <gde@llew.me>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
---
|
|
||||||
module: openssl_pkcs12
|
|
||||||
author:
|
|
||||||
- Guillaume Delpierre (@gdelpierre)
|
|
||||||
version_added: "2.7"
|
|
||||||
short_description: Generate OpenSSL PKCS#12 archive
|
|
||||||
description:
|
|
||||||
- This module allows one to (re-)generate PKCS#12.
|
|
||||||
requirements:
|
|
||||||
- python-pyOpenSSL
|
|
||||||
options:
|
|
||||||
action:
|
|
||||||
description:
|
|
||||||
- C(export) or C(parse) a PKCS#12.
|
|
||||||
type: str
|
|
||||||
default: export
|
|
||||||
choices: [ export, parse ]
|
|
||||||
other_certificates:
|
|
||||||
description:
|
|
||||||
- List of other certificates to include. Pre 2.8 this parameter was called C(ca_certificates)
|
|
||||||
type: list
|
|
||||||
elements: path
|
|
||||||
aliases: [ ca_certificates ]
|
|
||||||
certificate_path:
|
|
||||||
description:
|
|
||||||
- The path to read certificates and private keys from.
|
|
||||||
- Must be in PEM format.
|
|
||||||
type: path
|
|
||||||
force:
|
|
||||||
description:
|
|
||||||
- Should the file be regenerated even if it already exists.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
friendly_name:
|
|
||||||
description:
|
|
||||||
- Specifies the friendly name for the certificate and private key.
|
|
||||||
type: str
|
|
||||||
aliases: [ name ]
|
|
||||||
iter_size:
|
|
||||||
description:
|
|
||||||
- Number of times to repeat the encryption step.
|
|
||||||
type: int
|
|
||||||
default: 2048
|
|
||||||
maciter_size:
|
|
||||||
description:
|
|
||||||
- Number of times to repeat the MAC step.
|
|
||||||
type: int
|
|
||||||
default: 1
|
|
||||||
passphrase:
|
|
||||||
description:
|
|
||||||
- The PKCS#12 password.
|
|
||||||
type: str
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- Filename to write the PKCS#12 file to.
|
|
||||||
type: path
|
|
||||||
required: true
|
|
||||||
privatekey_passphrase:
|
|
||||||
description:
|
|
||||||
- Passphrase source to decrypt any input private keys with.
|
|
||||||
type: str
|
|
||||||
privatekey_path:
|
|
||||||
description:
|
|
||||||
- File to read private key from.
|
|
||||||
type: path
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- Whether the file should exist or not.
|
|
||||||
All parameters except C(path) are ignored when state is C(absent).
|
|
||||||
choices: [ absent, present ]
|
|
||||||
default: present
|
|
||||||
type: str
|
|
||||||
src:
|
|
||||||
description:
|
|
||||||
- PKCS#12 file path to parse.
|
|
||||||
type: path
|
|
||||||
backup:
|
|
||||||
description:
|
|
||||||
- Create a backup file including a timestamp so you can get the original
|
|
||||||
output file back if you overwrote it with a new one by accident.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
version_added: "2.8"
|
|
||||||
return_content:
|
|
||||||
description:
|
|
||||||
- If set to C(yes), will return the (current or generated) PKCS#12's content as I(pkcs12).
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
version_added: "2.10"
|
|
||||||
extends_documentation_fragment:
|
|
||||||
- files
|
|
||||||
seealso:
|
|
||||||
- module: openssl_certificate
|
|
||||||
- module: openssl_csr
|
|
||||||
- module: openssl_dhparam
|
|
||||||
- module: openssl_privatekey
|
|
||||||
- module: openssl_publickey
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Generate PKCS#12 file
|
|
||||||
openssl_pkcs12:
|
|
||||||
action: export
|
|
||||||
path: /opt/certs/ansible.p12
|
|
||||||
friendly_name: raclette
|
|
||||||
privatekey_path: /opt/certs/keys/key.pem
|
|
||||||
certificate_path: /opt/certs/cert.pem
|
|
||||||
other_certificates: /opt/certs/ca.pem
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Change PKCS#12 file permission
|
|
||||||
openssl_pkcs12:
|
|
||||||
action: export
|
|
||||||
path: /opt/certs/ansible.p12
|
|
||||||
friendly_name: raclette
|
|
||||||
privatekey_path: /opt/certs/keys/key.pem
|
|
||||||
certificate_path: /opt/certs/cert.pem
|
|
||||||
other_certificates: /opt/certs/ca.pem
|
|
||||||
state: present
|
|
||||||
mode: '0600'
|
|
||||||
|
|
||||||
- name: Regen PKCS#12 file
|
|
||||||
openssl_pkcs12:
|
|
||||||
action: export
|
|
||||||
src: /opt/certs/ansible.p12
|
|
||||||
path: /opt/certs/ansible.p12
|
|
||||||
friendly_name: raclette
|
|
||||||
privatekey_path: /opt/certs/keys/key.pem
|
|
||||||
certificate_path: /opt/certs/cert.pem
|
|
||||||
other_certificates: /opt/certs/ca.pem
|
|
||||||
state: present
|
|
||||||
mode: '0600'
|
|
||||||
force: yes
|
|
||||||
|
|
||||||
- name: Dump/Parse PKCS#12 file
|
|
||||||
openssl_pkcs12:
|
|
||||||
action: parse
|
|
||||||
src: /opt/certs/ansible.p12
|
|
||||||
path: /opt/certs/ansible.pem
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Remove PKCS#12 file
|
|
||||||
openssl_pkcs12:
|
|
||||||
path: /opt/certs/ansible.p12
|
|
||||||
state: absent
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = r'''
|
|
||||||
filename:
|
|
||||||
description: Path to the generate PKCS#12 file.
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: /opt/certs/ansible.p12
|
|
||||||
privatekey:
|
|
||||||
description: Path to the TLS/SSL private key the public key was generated from.
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: /etc/ssl/private/ansible.com.pem
|
|
||||||
backup_file:
|
|
||||||
description: Name of backup file created.
|
|
||||||
returned: changed and if I(backup) is C(yes)
|
|
||||||
type: str
|
|
||||||
sample: /path/to/ansible.com.pem.2019-03-09@11:22~
|
|
||||||
pkcs12:
|
|
||||||
description: The (current or generated) PKCS#12's content Base64 encoded.
|
|
||||||
returned: if I(state) is C(present) and I(return_content) is C(yes)
|
|
||||||
type: str
|
|
||||||
version_added: "2.10"
|
|
||||||
'''
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import stat
|
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
PYOPENSSL_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
from OpenSSL import crypto
|
|
||||||
except ImportError:
|
|
||||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
|
||||||
pyopenssl_found = False
|
|
||||||
else:
|
|
||||||
pyopenssl_found = True
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
from ansible.module_utils import crypto as crypto_utils
|
|
||||||
from ansible.module_utils._text import to_bytes, to_native
|
|
||||||
|
|
||||||
|
|
||||||
class PkcsError(crypto_utils.OpenSSLObjectError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Pkcs(crypto_utils.OpenSSLObject):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(Pkcs, self).__init__(
|
|
||||||
module.params['path'],
|
|
||||||
module.params['state'],
|
|
||||||
module.params['force'],
|
|
||||||
module.check_mode
|
|
||||||
)
|
|
||||||
self.action = module.params['action']
|
|
||||||
self.other_certificates = module.params['other_certificates']
|
|
||||||
self.certificate_path = module.params['certificate_path']
|
|
||||||
self.friendly_name = module.params['friendly_name']
|
|
||||||
self.iter_size = module.params['iter_size']
|
|
||||||
self.maciter_size = module.params['maciter_size']
|
|
||||||
self.passphrase = module.params['passphrase']
|
|
||||||
self.pkcs12 = None
|
|
||||||
self.privatekey_passphrase = module.params['privatekey_passphrase']
|
|
||||||
self.privatekey_path = module.params['privatekey_path']
|
|
||||||
self.pkcs12_bytes = None
|
|
||||||
self.return_content = module.params['return_content']
|
|
||||||
self.src = module.params['src']
|
|
||||||
|
|
||||||
if module.params['mode'] is None:
|
|
||||||
module.params['mode'] = '0400'
|
|
||||||
|
|
||||||
self.backup = module.params['backup']
|
|
||||||
self.backup_file = None
|
|
||||||
|
|
||||||
def check(self, module, perms_required=True):
|
|
||||||
"""Ensure the resource is in its desired state."""
|
|
||||||
|
|
||||||
state_and_perms = super(Pkcs, self).check(module, perms_required)
|
|
||||||
|
|
||||||
def _check_pkey_passphrase():
|
|
||||||
if self.privatekey_passphrase:
|
|
||||||
try:
|
|
||||||
crypto_utils.load_privatekey(self.privatekey_path,
|
|
||||||
self.privatekey_passphrase)
|
|
||||||
except crypto.Error:
|
|
||||||
return False
|
|
||||||
except crypto_utils.OpenSSLBadPassphraseError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not state_and_perms:
|
|
||||||
return state_and_perms
|
|
||||||
|
|
||||||
if os.path.exists(self.path) and module.params['action'] == 'export':
|
|
||||||
dummy = self.generate(module)
|
|
||||||
self.src = self.path
|
|
||||||
try:
|
|
||||||
pkcs12_privatekey, pkcs12_certificate, pkcs12_other_certificates, pkcs12_friendly_name = self.parse()
|
|
||||||
except crypto.Error:
|
|
||||||
return False
|
|
||||||
if (pkcs12_privatekey is not None) and (self.privatekey_path is not None):
|
|
||||||
expected_pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
|
||||||
self.pkcs12.get_privatekey())
|
|
||||||
if pkcs12_privatekey != expected_pkey:
|
|
||||||
return False
|
|
||||||
elif bool(pkcs12_privatekey) != bool(self.privatekey_path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if (pkcs12_certificate is not None) and (self.certificate_path is not None):
|
|
||||||
|
|
||||||
expected_cert = crypto.dump_certificate(crypto.FILETYPE_PEM,
|
|
||||||
self.pkcs12.get_certificate())
|
|
||||||
if pkcs12_certificate != expected_cert:
|
|
||||||
return False
|
|
||||||
elif bool(pkcs12_certificate) != bool(self.certificate_path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if (pkcs12_other_certificates is not None) and (self.other_certificates is not None):
|
|
||||||
expected_other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM,
|
|
||||||
other_cert) for other_cert in self.pkcs12.get_ca_certificates()]
|
|
||||||
if set(pkcs12_other_certificates) != set(expected_other_certs):
|
|
||||||
return False
|
|
||||||
elif bool(pkcs12_other_certificates) != bool(self.other_certificates):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if pkcs12_privatekey:
|
|
||||||
# This check is required because pyOpenSSL will not return a friendly name
|
|
||||||
# if the private key is not set in the file
|
|
||||||
if ((self.pkcs12.get_friendlyname() is not None) and (pkcs12_friendly_name is not None)):
|
|
||||||
if self.pkcs12.get_friendlyname() != pkcs12_friendly_name:
|
|
||||||
return False
|
|
||||||
elif bool(self.pkcs12.get_friendlyname()) != bool(pkcs12_friendly_name):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return _check_pkey_passphrase()
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
"""Serialize the object into a dictionary."""
|
|
||||||
|
|
||||||
result = {
|
|
||||||
'filename': self.path,
|
|
||||||
}
|
|
||||||
if self.privatekey_path:
|
|
||||||
result['privatekey_path'] = self.privatekey_path
|
|
||||||
if self.backup_file:
|
|
||||||
result['backup_file'] = self.backup_file
|
|
||||||
if self.return_content:
|
|
||||||
if self.pkcs12_bytes is None:
|
|
||||||
self.pkcs12_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True)
|
|
||||||
result['pkcs12'] = base64.b64encode(self.pkcs12_bytes) if self.pkcs12_bytes else None
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def generate(self, module):
|
|
||||||
"""Generate PKCS#12 file archive."""
|
|
||||||
self.pkcs12 = crypto.PKCS12()
|
|
||||||
|
|
||||||
if self.other_certificates:
|
|
||||||
other_certs = [crypto_utils.load_certificate(other_cert) for other_cert
|
|
||||||
in self.other_certificates]
|
|
||||||
self.pkcs12.set_ca_certificates(other_certs)
|
|
||||||
|
|
||||||
if self.certificate_path:
|
|
||||||
self.pkcs12.set_certificate(crypto_utils.load_certificate(
|
|
||||||
self.certificate_path))
|
|
||||||
|
|
||||||
if self.friendly_name:
|
|
||||||
self.pkcs12.set_friendlyname(to_bytes(self.friendly_name))
|
|
||||||
|
|
||||||
if self.privatekey_path:
|
|
||||||
try:
|
|
||||||
self.pkcs12.set_privatekey(crypto_utils.load_privatekey(
|
|
||||||
self.privatekey_path,
|
|
||||||
self.privatekey_passphrase)
|
|
||||||
)
|
|
||||||
except crypto_utils.OpenSSLBadPassphraseError as exc:
|
|
||||||
raise PkcsError(exc)
|
|
||||||
|
|
||||||
return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size)
|
|
||||||
|
|
||||||
def remove(self, module):
|
|
||||||
if self.backup:
|
|
||||||
self.backup_file = module.backup_local(self.path)
|
|
||||||
super(Pkcs, self).remove(module)
|
|
||||||
|
|
||||||
def parse(self):
|
|
||||||
"""Read PKCS#12 file."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(self.src, 'rb') as pkcs12_fh:
|
|
||||||
pkcs12_content = pkcs12_fh.read()
|
|
||||||
p12 = crypto.load_pkcs12(pkcs12_content,
|
|
||||||
self.passphrase)
|
|
||||||
pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
|
||||||
p12.get_privatekey())
|
|
||||||
crt = crypto.dump_certificate(crypto.FILETYPE_PEM,
|
|
||||||
p12.get_certificate())
|
|
||||||
other_certs = []
|
|
||||||
if p12.get_ca_certificates() is not None:
|
|
||||||
other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM,
|
|
||||||
other_cert) for other_cert in p12.get_ca_certificates()]
|
|
||||||
|
|
||||||
friendly_name = p12.get_friendlyname()
|
|
||||||
|
|
||||||
return (pkey, crt, other_certs, friendly_name)
|
|
||||||
|
|
||||||
except IOError as exc:
|
|
||||||
raise PkcsError(exc)
|
|
||||||
|
|
||||||
def write(self, module, content, mode=None):
|
|
||||||
"""Write the PKCS#12 file."""
|
|
||||||
if self.backup:
|
|
||||||
self.backup_file = module.backup_local(self.path)
|
|
||||||
crypto_utils.write_file(module, content, mode)
|
|
||||||
if self.return_content:
|
|
||||||
self.pkcs12_bytes = content
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
argument_spec = dict(
|
|
||||||
action=dict(type='str', default='export', choices=['export', 'parse']),
|
|
||||||
other_certificates=dict(type='list', elements='path', aliases=['ca_certificates']),
|
|
||||||
certificate_path=dict(type='path'),
|
|
||||||
force=dict(type='bool', default=False),
|
|
||||||
friendly_name=dict(type='str', aliases=['name']),
|
|
||||||
iter_size=dict(type='int', default=2048),
|
|
||||||
maciter_size=dict(type='int', default=1),
|
|
||||||
passphrase=dict(type='str', no_log=True),
|
|
||||||
path=dict(type='path', required=True),
|
|
||||||
privatekey_passphrase=dict(type='str', no_log=True),
|
|
||||||
privatekey_path=dict(type='path'),
|
|
||||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
|
||||||
src=dict(type='path'),
|
|
||||||
backup=dict(type='bool', default=False),
|
|
||||||
return_content=dict(type='bool', default=False),
|
|
||||||
)
|
|
||||||
|
|
||||||
required_if = [
|
|
||||||
['action', 'parse', ['src']],
|
|
||||||
]
|
|
||||||
|
|
||||||
module = AnsibleModule(
|
|
||||||
add_file_common_args=True,
|
|
||||||
argument_spec=argument_spec,
|
|
||||||
required_if=required_if,
|
|
||||||
supports_check_mode=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not pyopenssl_found:
|
|
||||||
module.fail_json(msg=missing_required_lib('pyOpenSSL'), exception=PYOPENSSL_IMP_ERR)
|
|
||||||
|
|
||||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
|
||||||
if not os.path.isdir(base_dir):
|
|
||||||
module.fail_json(
|
|
||||||
name=base_dir,
|
|
||||||
msg="The directory '%s' does not exist or the path is not a directory" % base_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
pkcs12 = Pkcs(module)
|
|
||||||
changed = False
|
|
||||||
|
|
||||||
if module.params['state'] == 'present':
|
|
||||||
if module.check_mode:
|
|
||||||
result = pkcs12.dump()
|
|
||||||
result['changed'] = module.params['force'] or not pkcs12.check(module)
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
if not pkcs12.check(module, perms_required=False) or module.params['force']:
|
|
||||||
if module.params['action'] == 'export':
|
|
||||||
if not module.params['friendly_name']:
|
|
||||||
module.fail_json(msg='Friendly_name is required')
|
|
||||||
pkcs12_content = pkcs12.generate(module)
|
|
||||||
pkcs12.write(module, pkcs12_content, 0o600)
|
|
||||||
changed = True
|
|
||||||
else:
|
|
||||||
pkey, cert, other_certs, friendly_name = pkcs12.parse()
|
|
||||||
dump_content = '%s%s%s' % (to_native(pkey), to_native(cert), to_native(b''.join(other_certs)))
|
|
||||||
pkcs12.write(module, to_bytes(dump_content))
|
|
||||||
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
|
||||||
if module.set_fs_attributes_if_different(file_args, changed):
|
|
||||||
changed = True
|
|
||||||
else:
|
|
||||||
if module.check_mode:
|
|
||||||
result = pkcs12.dump()
|
|
||||||
result['changed'] = os.path.exists(module.params['path'])
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
if os.path.exists(module.params['path']):
|
|
||||||
pkcs12.remove(module)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
result = pkcs12.dump()
|
|
||||||
result['changed'] = changed
|
|
||||||
if os.path.exists(module.params['path']):
|
|
||||||
file_mode = "%04o" % stat.S_IMODE(os.stat(module.params['path']).st_mode)
|
|
||||||
result['mode'] = file_mode
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
except crypto_utils.OpenSSLObjectError as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,943 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright: (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
---
|
|
||||||
module: openssl_privatekey
|
|
||||||
version_added: "2.3"
|
|
||||||
short_description: Generate OpenSSL private keys
|
|
||||||
description:
|
|
||||||
- This module allows one to (re)generate OpenSSL private keys.
|
|
||||||
- One can generate L(RSA,https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29),
|
|
||||||
L(DSA,https://en.wikipedia.org/wiki/Digital_Signature_Algorithm),
|
|
||||||
L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) or
|
|
||||||
L(EdDSA,https://en.wikipedia.org/wiki/EdDSA) private keys.
|
|
||||||
- Keys are generated in PEM format.
|
|
||||||
- "Please note that the module regenerates private keys if they don't match
|
|
||||||
the module's options. In particular, if you provide another passphrase
|
|
||||||
(or specify none), change the keysize, etc., the private key will be
|
|
||||||
regenerated. If you are concerned that this could **overwrite your private key**,
|
|
||||||
consider using the I(backup) option."
|
|
||||||
- The module can use the cryptography Python library, or the pyOpenSSL Python
|
|
||||||
library. By default, it tries to detect which one is available. This can be
|
|
||||||
overridden with the I(select_crypto_backend) option. Please note that the
|
|
||||||
PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in Ansible 2.13."
|
|
||||||
requirements:
|
|
||||||
- Either cryptography >= 1.2.3 (older versions might work as well)
|
|
||||||
- Or pyOpenSSL
|
|
||||||
author:
|
|
||||||
- Yanis Guenane (@Spredzy)
|
|
||||||
- Felix Fontein (@felixfontein)
|
|
||||||
options:
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- Whether the private key should exist or not, taking action if the state is different from what is stated.
|
|
||||||
type: str
|
|
||||||
default: present
|
|
||||||
choices: [ absent, present ]
|
|
||||||
size:
|
|
||||||
description:
|
|
||||||
- Size (in bits) of the TLS/SSL key to generate.
|
|
||||||
type: int
|
|
||||||
default: 4096
|
|
||||||
type:
|
|
||||||
description:
|
|
||||||
- The algorithm used to generate the TLS/SSL private key.
|
|
||||||
- Note that C(ECC), C(X25519), C(X448), C(Ed25519) and C(Ed448) require the C(cryptography) backend.
|
|
||||||
C(X25519) needs cryptography 2.5 or newer, while C(X448), C(Ed25519) and C(Ed448) require
|
|
||||||
cryptography 2.6 or newer. For C(ECC), the minimal cryptography version required depends on the
|
|
||||||
I(curve) option.
|
|
||||||
type: str
|
|
||||||
default: RSA
|
|
||||||
choices: [ DSA, ECC, Ed25519, Ed448, RSA, X25519, X448 ]
|
|
||||||
curve:
|
|
||||||
description:
|
|
||||||
- Note that not all curves are supported by all versions of C(cryptography).
|
|
||||||
- For maximal interoperability, C(secp384r1) or C(secp256r1) should be used.
|
|
||||||
- We use the curve names as defined in the
|
|
||||||
L(IANA registry for TLS,https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8).
|
|
||||||
type: str
|
|
||||||
choices:
|
|
||||||
- secp384r1
|
|
||||||
- secp521r1
|
|
||||||
- secp224r1
|
|
||||||
- secp192r1
|
|
||||||
- secp256r1
|
|
||||||
- secp256k1
|
|
||||||
- brainpoolP256r1
|
|
||||||
- brainpoolP384r1
|
|
||||||
- brainpoolP512r1
|
|
||||||
- sect571k1
|
|
||||||
- sect409k1
|
|
||||||
- sect283k1
|
|
||||||
- sect233k1
|
|
||||||
- sect163k1
|
|
||||||
- sect571r1
|
|
||||||
- sect409r1
|
|
||||||
- sect283r1
|
|
||||||
- sect233r1
|
|
||||||
- sect163r2
|
|
||||||
version_added: "2.8"
|
|
||||||
force:
|
|
||||||
description:
|
|
||||||
- Should the key be regenerated even if it already exists.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- Name of the file in which the generated TLS/SSL private key will be written. It will have 0600 mode.
|
|
||||||
type: path
|
|
||||||
required: true
|
|
||||||
passphrase:
|
|
||||||
description:
|
|
||||||
- The passphrase for the private key.
|
|
||||||
type: str
|
|
||||||
version_added: "2.4"
|
|
||||||
cipher:
|
|
||||||
description:
|
|
||||||
- The cipher to encrypt the private key. (Valid values can be found by
|
|
||||||
running `openssl list -cipher-algorithms` or `openssl list-cipher-algorithms`,
|
|
||||||
depending on your OpenSSL version.)
|
|
||||||
- When using the C(cryptography) backend, use C(auto).
|
|
||||||
type: str
|
|
||||||
version_added: "2.4"
|
|
||||||
select_crypto_backend:
|
|
||||||
description:
|
|
||||||
- Determines which crypto backend to use.
|
|
||||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
|
||||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
|
||||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
|
||||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13.
|
|
||||||
From that point on, only the C(cryptography) backend will be available.
|
|
||||||
type: str
|
|
||||||
default: auto
|
|
||||||
choices: [ auto, cryptography, pyopenssl ]
|
|
||||||
version_added: "2.8"
|
|
||||||
format:
|
|
||||||
description:
|
|
||||||
- Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format)
|
|
||||||
is used for all keys which support it. Please note that not every key can be exported in any format.
|
|
||||||
- The value C(auto) selects a fromat based on the key format. The value C(auto_ignore) does the same,
|
|
||||||
but for existing private key files, it will not force a regenerate when its format is not the automatically
|
|
||||||
selected one for generation.
|
|
||||||
- Note that if the format for an existing private key mismatches, the key is *regenerated* by default.
|
|
||||||
To change this behavior, use the I(format_mismatch) option.
|
|
||||||
- The I(format) option is only supported by the C(cryptography) backend. The C(pyopenssl) backend will
|
|
||||||
fail if a value different from C(auto_ignore) is used.
|
|
||||||
type: str
|
|
||||||
default: auto_ignore
|
|
||||||
choices: [ pkcs1, pkcs8, raw, auto, auto_ignore ]
|
|
||||||
version_added: "2.10"
|
|
||||||
format_mismatch:
|
|
||||||
description:
|
|
||||||
- Determines behavior of the module if the format of a private key does not match the expected format, but all
|
|
||||||
other parameters are as expected.
|
|
||||||
- If set to C(regenerate) (default), generates a new private key.
|
|
||||||
- If set to C(convert), the key will be converted to the new format instead.
|
|
||||||
- Only supported by the C(cryptography) backend.
|
|
||||||
type: str
|
|
||||||
default: regenerate
|
|
||||||
choices: [ regenerate, convert ]
|
|
||||||
version_added: "2.10"
|
|
||||||
backup:
|
|
||||||
description:
|
|
||||||
- Create a backup file including a timestamp so you can get
|
|
||||||
the original private key back if you overwrote it with a new one by accident.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
version_added: "2.8"
|
|
||||||
return_content:
|
|
||||||
description:
|
|
||||||
- If set to C(yes), will return the (current or generated) private key's content as I(privatekey).
|
|
||||||
- Note that especially if the private key is not encrypted, you have to make sure that the returned
|
|
||||||
value is treated appropriately and not accidentally written to logs etc.! Use with care!
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
version_added: "2.10"
|
|
||||||
regenerate:
|
|
||||||
description:
|
|
||||||
- Allows to configure in which situations the module is allowed to regenerate private keys.
|
|
||||||
The module will always generate a new key if the destination file does not exist.
|
|
||||||
- By default, the key will be regenerated when it doesn't match the module's options,
|
|
||||||
except when the key cannot be read or the passphrase does not match. Please note that
|
|
||||||
this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence)
|
|
||||||
is specified.
|
|
||||||
- If set to C(never), the module will fail if the key cannot be read or the passphrase
|
|
||||||
isn't matching, and will never regenerate an existing key.
|
|
||||||
- If set to C(fail), the module will fail if the key does not correspond to the module's
|
|
||||||
options.
|
|
||||||
- If set to C(partial_idempotence), the key will be regenerated if it does not conform to
|
|
||||||
the module's options. The key is B(not) regenerated if it cannot be read (broken file),
|
|
||||||
the key is protected by an unknown passphrase, or when they key is not protected by a
|
|
||||||
passphrase, but a passphrase is specified.
|
|
||||||
- If set to C(full_idempotence), the key will be regenerated if it does not conform to the
|
|
||||||
module's options. This is also the case if the key cannot be read (broken file), the key
|
|
||||||
is protected by an unknown passphrase, or when they key is not protected by a passphrase,
|
|
||||||
but a passphrase is specified. Make sure you have a B(backup) when using this option!
|
|
||||||
- If set to C(always), the module will always regenerate the key. This is equivalent to
|
|
||||||
setting I(force) to C(yes).
|
|
||||||
- Note that if I(format_mismatch) is set to C(convert) and everything matches except the
|
|
||||||
format, the key will always be converted, except if I(regenerate) is set to C(always).
|
|
||||||
type: str
|
|
||||||
choices:
|
|
||||||
- never
|
|
||||||
- fail
|
|
||||||
- partial_idempotence
|
|
||||||
- full_idempotence
|
|
||||||
- always
|
|
||||||
default: full_idempotence
|
|
||||||
version_added: '2.10'
|
|
||||||
extends_documentation_fragment:
|
|
||||||
- files
|
|
||||||
seealso:
|
|
||||||
- module: openssl_certificate
|
|
||||||
- module: openssl_csr
|
|
||||||
- module: openssl_dhparam
|
|
||||||
- module: openssl_pkcs12
|
|
||||||
- module: openssl_publickey
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Generate an OpenSSL private key with the default values (4096 bits, RSA)
|
|
||||||
openssl_privatekey:
|
|
||||||
path: /etc/ssl/private/ansible.com.pem
|
|
||||||
|
|
||||||
- name: Generate an OpenSSL private key with the default values (4096 bits, RSA) and a passphrase
|
|
||||||
openssl_privatekey:
|
|
||||||
path: /etc/ssl/private/ansible.com.pem
|
|
||||||
passphrase: ansible
|
|
||||||
cipher: aes256
|
|
||||||
|
|
||||||
- name: Generate an OpenSSL private key with a different size (2048 bits)
|
|
||||||
openssl_privatekey:
|
|
||||||
path: /etc/ssl/private/ansible.com.pem
|
|
||||||
size: 2048
|
|
||||||
|
|
||||||
- name: Force regenerate an OpenSSL private key if it already exists
|
|
||||||
openssl_privatekey:
|
|
||||||
path: /etc/ssl/private/ansible.com.pem
|
|
||||||
force: yes
|
|
||||||
|
|
||||||
- name: Generate an OpenSSL private key with a different algorithm (DSA)
|
|
||||||
openssl_privatekey:
|
|
||||||
path: /etc/ssl/private/ansible.com.pem
|
|
||||||
type: DSA
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = r'''
|
|
||||||
size:
|
|
||||||
description: Size (in bits) of the TLS/SSL private key.
|
|
||||||
returned: changed or success
|
|
||||||
type: int
|
|
||||||
sample: 4096
|
|
||||||
type:
|
|
||||||
description: Algorithm used to generate the TLS/SSL private key.
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: RSA
|
|
||||||
curve:
|
|
||||||
description: Elliptic curve used to generate the TLS/SSL private key.
|
|
||||||
returned: changed or success, and I(type) is C(ECC)
|
|
||||||
type: str
|
|
||||||
sample: secp256r1
|
|
||||||
filename:
|
|
||||||
description: Path to the generated TLS/SSL private key file.
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: /etc/ssl/private/ansible.com.pem
|
|
||||||
fingerprint:
|
|
||||||
description:
|
|
||||||
- The fingerprint of the public key. Fingerprint will be generated for each C(hashlib.algorithms) available.
|
|
||||||
- The PyOpenSSL backend requires PyOpenSSL >= 16.0 for meaningful output.
|
|
||||||
returned: changed or success
|
|
||||||
type: dict
|
|
||||||
sample:
|
|
||||||
md5: "84:75:71:72:8d:04:b5:6c:4d:37:6d:66:83:f5:4c:29"
|
|
||||||
sha1: "51:cc:7c:68:5d:eb:41:43:88:7e:1a:ae:c7:f8:24:72:ee:71:f6:10"
|
|
||||||
sha224: "b1:19:a6:6c:14:ac:33:1d:ed:18:50:d3:06:5c:b2:32:91:f1:f1:52:8c:cb:d5:75:e9:f5:9b:46"
|
|
||||||
sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7"
|
|
||||||
sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d"
|
|
||||||
sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b"
|
|
||||||
backup_file:
|
|
||||||
description: Name of backup file created.
|
|
||||||
returned: changed and if I(backup) is C(yes)
|
|
||||||
type: str
|
|
||||||
sample: /path/to/privatekey.pem.2019-03-09@11:22~
|
|
||||||
privatekey:
|
|
||||||
description:
|
|
||||||
- The (current or generated) private key's content.
|
|
||||||
- Will be Base64-encoded if the key is in raw format.
|
|
||||||
returned: if I(state) is C(present) and I(return_content) is C(yes)
|
|
||||||
type: str
|
|
||||||
version_added: "2.10"
|
|
||||||
'''
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import base64
|
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
|
|
||||||
MINIMAL_PYOPENSSL_VERSION = '0.6'
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
|
||||||
|
|
||||||
PYOPENSSL_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import OpenSSL
|
|
||||||
from OpenSSL import crypto
|
|
||||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
|
||||||
except ImportError:
|
|
||||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
|
||||||
PYOPENSSL_FOUND = False
|
|
||||||
else:
|
|
||||||
PYOPENSSL_FOUND = True
|
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
import cryptography.exceptions
|
|
||||||
import cryptography.hazmat.backends
|
|
||||||
import cryptography.hazmat.primitives.serialization
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.dsa
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.ec
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.utils
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
from ansible.module_utils.crypto import (
|
|
||||||
CRYPTOGRAPHY_HAS_X25519,
|
|
||||||
CRYPTOGRAPHY_HAS_X25519_FULL,
|
|
||||||
CRYPTOGRAPHY_HAS_X448,
|
|
||||||
CRYPTOGRAPHY_HAS_ED25519,
|
|
||||||
CRYPTOGRAPHY_HAS_ED448,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ansible.module_utils import crypto as crypto_utils
|
|
||||||
from ansible.module_utils._text import to_native, to_bytes
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
|
|
||||||
|
|
||||||
class PrivateKeyError(crypto_utils.OpenSSLObjectError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PrivateKeyBase(crypto_utils.OpenSSLObject):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(PrivateKeyBase, self).__init__(
|
|
||||||
module.params['path'],
|
|
||||||
module.params['state'],
|
|
||||||
module.params['force'],
|
|
||||||
module.check_mode
|
|
||||||
)
|
|
||||||
self.size = module.params['size']
|
|
||||||
self.passphrase = module.params['passphrase']
|
|
||||||
self.cipher = module.params['cipher']
|
|
||||||
self.privatekey = None
|
|
||||||
self.fingerprint = {}
|
|
||||||
self.format = module.params['format']
|
|
||||||
self.format_mismatch = module.params['format_mismatch']
|
|
||||||
self.privatekey_bytes = None
|
|
||||||
self.return_content = module.params['return_content']
|
|
||||||
self.regenerate = module.params['regenerate']
|
|
||||||
if self.regenerate == 'always':
|
|
||||||
self.force = True
|
|
||||||
|
|
||||||
self.backup = module.params['backup']
|
|
||||||
self.backup_file = None
|
|
||||||
|
|
||||||
if module.params['mode'] is None:
|
|
||||||
module.params['mode'] = '0600'
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _generate_private_key(self):
|
|
||||||
"""(Re-)Generate private key."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _ensure_private_key_loaded(self):
|
|
||||||
"""Make sure that the private key has been loaded."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_private_key_data(self):
|
|
||||||
"""Return bytes for self.privatekey"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_fingerprint(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def generate(self, module):
|
|
||||||
"""Generate a keypair."""
|
|
||||||
|
|
||||||
if not self.check(module, perms_required=False, ignore_conversion=True) or self.force:
|
|
||||||
# Regenerate
|
|
||||||
if self.backup:
|
|
||||||
self.backup_file = module.backup_local(self.path)
|
|
||||||
self._generate_private_key()
|
|
||||||
privatekey_data = self._get_private_key_data()
|
|
||||||
if self.return_content:
|
|
||||||
self.privatekey_bytes = privatekey_data
|
|
||||||
crypto_utils.write_file(module, privatekey_data, 0o600)
|
|
||||||
self.changed = True
|
|
||||||
elif not self.check(module, perms_required=False, ignore_conversion=False):
|
|
||||||
# Convert
|
|
||||||
if self.backup:
|
|
||||||
self.backup_file = module.backup_local(self.path)
|
|
||||||
self._ensure_private_key_loaded()
|
|
||||||
privatekey_data = self._get_private_key_data()
|
|
||||||
if self.return_content:
|
|
||||||
self.privatekey_bytes = privatekey_data
|
|
||||||
crypto_utils.write_file(module, privatekey_data, 0o600)
|
|
||||||
self.changed = True
|
|
||||||
|
|
||||||
self.fingerprint = self._get_fingerprint()
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
|
||||||
if module.set_fs_attributes_if_different(file_args, False):
|
|
||||||
self.changed = True
|
|
||||||
|
|
||||||
def remove(self, module):
|
|
||||||
if self.backup:
|
|
||||||
self.backup_file = module.backup_local(self.path)
|
|
||||||
super(PrivateKeyBase, self).remove(module)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _check_passphrase(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _check_size_and_type(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _check_format(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def check(self, module, perms_required=True, ignore_conversion=True):
|
|
||||||
"""Ensure the resource is in its desired state."""
|
|
||||||
|
|
||||||
state_and_perms = super(PrivateKeyBase, self).check(module, perms_required=False)
|
|
||||||
|
|
||||||
if not state_and_perms:
|
|
||||||
# key does not exist
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not self._check_passphrase():
|
|
||||||
if self.regenerate in ('full_idempotence', 'always'):
|
|
||||||
return False
|
|
||||||
module.fail_json(msg='Unable to read the key. The key is protected with a another passphrase / no passphrase or broken.'
|
|
||||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
|
||||||
' set to `full_idempotence` or `always`, or with `force=yes`.')
|
|
||||||
|
|
||||||
if self.regenerate != 'never':
|
|
||||||
if not self._check_size_and_type():
|
|
||||||
if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'):
|
|
||||||
return False
|
|
||||||
module.fail_json(msg='Key has wrong type and/or size.'
|
|
||||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
|
||||||
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.')
|
|
||||||
|
|
||||||
if not self._check_format():
|
|
||||||
# During conversion step, convert if format does not match and format_mismatch == 'convert'
|
|
||||||
if not ignore_conversion and self.format_mismatch == 'convert':
|
|
||||||
return False
|
|
||||||
# During generation step, regenerate if format does not match and format_mismatch == 'regenerate'
|
|
||||||
if ignore_conversion and self.format_mismatch == 'regenerate' and self.regenerate != 'never':
|
|
||||||
if not ignore_conversion or self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'):
|
|
||||||
return False
|
|
||||||
module.fail_json(msg='Key has wrong format.'
|
|
||||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
|
||||||
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.'
|
|
||||||
' To convert the key, set `format_mismatch` to `convert`.')
|
|
||||||
|
|
||||||
# check whether permissions are correct (in case that needs to be checked)
|
|
||||||
return not perms_required or super(PrivateKeyBase, self).check(module, perms_required=perms_required)
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
"""Serialize the object into a dictionary."""
|
|
||||||
|
|
||||||
result = {
|
|
||||||
'size': self.size,
|
|
||||||
'filename': self.path,
|
|
||||||
'changed': self.changed,
|
|
||||||
'fingerprint': self.fingerprint,
|
|
||||||
}
|
|
||||||
if self.backup_file:
|
|
||||||
result['backup_file'] = self.backup_file
|
|
||||||
if self.return_content:
|
|
||||||
if self.privatekey_bytes is None:
|
|
||||||
self.privatekey_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True)
|
|
||||||
if self.privatekey_bytes:
|
|
||||||
if crypto_utils.identify_private_key_format(self.privatekey_bytes) == 'raw':
|
|
||||||
result['privatekey'] = base64.b64encode(self.privatekey_bytes)
|
|
||||||
else:
|
|
||||||
result['privatekey'] = self.privatekey_bytes.decode('utf-8')
|
|
||||||
else:
|
|
||||||
result['privatekey'] = None
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# Implementation with using pyOpenSSL
|
|
||||||
class PrivateKeyPyOpenSSL(PrivateKeyBase):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(PrivateKeyPyOpenSSL, self).__init__(module)
|
|
||||||
|
|
||||||
if module.params['type'] == 'RSA':
|
|
||||||
self.type = crypto.TYPE_RSA
|
|
||||||
elif module.params['type'] == 'DSA':
|
|
||||||
self.type = crypto.TYPE_DSA
|
|
||||||
else:
|
|
||||||
module.fail_json(msg="PyOpenSSL backend only supports RSA and DSA keys.")
|
|
||||||
|
|
||||||
if self.format != 'auto_ignore':
|
|
||||||
module.fail_json(msg="PyOpenSSL backend only supports auto_ignore format.")
|
|
||||||
|
|
||||||
def _generate_private_key(self):
|
|
||||||
"""(Re-)Generate private key."""
|
|
||||||
self.privatekey = crypto.PKey()
|
|
||||||
try:
|
|
||||||
self.privatekey.generate_key(self.type, self.size)
|
|
||||||
except (TypeError, ValueError) as exc:
|
|
||||||
raise PrivateKeyError(exc)
|
|
||||||
|
|
||||||
def _ensure_private_key_loaded(self):
|
|
||||||
"""Make sure that the private key has been loaded."""
|
|
||||||
if self.privatekey is None:
|
|
||||||
try:
|
|
||||||
self.privatekey = privatekey = crypto_utils.load_privatekey(self.path, self.passphrase)
|
|
||||||
except crypto_utils.OpenSSLBadPassphraseError as exc:
|
|
||||||
raise PrivateKeyError(exc)
|
|
||||||
|
|
||||||
def _get_private_key_data(self):
|
|
||||||
"""Return bytes for self.privatekey"""
|
|
||||||
if self.cipher and self.passphrase:
|
|
||||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey,
|
|
||||||
self.cipher, to_bytes(self.passphrase))
|
|
||||||
else:
|
|
||||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey)
|
|
||||||
|
|
||||||
def _get_fingerprint(self):
|
|
||||||
return crypto_utils.get_fingerprint(self.path, self.passphrase)
|
|
||||||
|
|
||||||
def _check_passphrase(self):
|
|
||||||
try:
|
|
||||||
crypto_utils.load_privatekey(self.path, self.passphrase)
|
|
||||||
return True
|
|
||||||
except Exception as dummy:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _check_size_and_type(self):
|
|
||||||
def _check_size(privatekey):
|
|
||||||
return self.size == privatekey.bits()
|
|
||||||
|
|
||||||
def _check_type(privatekey):
|
|
||||||
return self.type == privatekey.type()
|
|
||||||
|
|
||||||
self._ensure_private_key_loaded()
|
|
||||||
return _check_size(self.privatekey) and _check_type(self.privatekey)
|
|
||||||
|
|
||||||
def _check_format(self):
|
|
||||||
# Not supported by this backend
|
|
||||||
return True
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
"""Serialize the object into a dictionary."""
|
|
||||||
|
|
||||||
result = super(PrivateKeyPyOpenSSL, self).dump()
|
|
||||||
|
|
||||||
if self.type == crypto.TYPE_RSA:
|
|
||||||
result['type'] = 'RSA'
|
|
||||||
else:
|
|
||||||
result['type'] = 'DSA'
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# Implementation with using cryptography
|
|
||||||
class PrivateKeyCryptography(PrivateKeyBase):
|
|
||||||
|
|
||||||
def _get_ec_class(self, ectype):
|
|
||||||
ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(ectype)
|
|
||||||
if ecclass is None:
|
|
||||||
self.module.fail_json(msg='Your cryptography version does not support {0}'.format(ectype))
|
|
||||||
return ecclass
|
|
||||||
|
|
||||||
def _add_curve(self, name, ectype, deprecated=False):
|
|
||||||
def create(size):
|
|
||||||
ecclass = self._get_ec_class(ectype)
|
|
||||||
return ecclass()
|
|
||||||
|
|
||||||
def verify(privatekey):
|
|
||||||
ecclass = self._get_ec_class(ectype)
|
|
||||||
return isinstance(privatekey.private_numbers().public_numbers.curve, ecclass)
|
|
||||||
|
|
||||||
self.curves[name] = {
|
|
||||||
'create': create,
|
|
||||||
'verify': verify,
|
|
||||||
'deprecated': deprecated,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(PrivateKeyCryptography, self).__init__(module)
|
|
||||||
|
|
||||||
self.curves = dict()
|
|
||||||
self._add_curve('secp384r1', 'SECP384R1')
|
|
||||||
self._add_curve('secp521r1', 'SECP521R1')
|
|
||||||
self._add_curve('secp224r1', 'SECP224R1')
|
|
||||||
self._add_curve('secp192r1', 'SECP192R1')
|
|
||||||
self._add_curve('secp256r1', 'SECP256R1')
|
|
||||||
self._add_curve('secp256k1', 'SECP256K1')
|
|
||||||
self._add_curve('brainpoolP256r1', 'BrainpoolP256R1', deprecated=True)
|
|
||||||
self._add_curve('brainpoolP384r1', 'BrainpoolP384R1', deprecated=True)
|
|
||||||
self._add_curve('brainpoolP512r1', 'BrainpoolP512R1', deprecated=True)
|
|
||||||
self._add_curve('sect571k1', 'SECT571K1', deprecated=True)
|
|
||||||
self._add_curve('sect409k1', 'SECT409K1', deprecated=True)
|
|
||||||
self._add_curve('sect283k1', 'SECT283K1', deprecated=True)
|
|
||||||
self._add_curve('sect233k1', 'SECT233K1', deprecated=True)
|
|
||||||
self._add_curve('sect163k1', 'SECT163K1', deprecated=True)
|
|
||||||
self._add_curve('sect571r1', 'SECT571R1', deprecated=True)
|
|
||||||
self._add_curve('sect409r1', 'SECT409R1', deprecated=True)
|
|
||||||
self._add_curve('sect283r1', 'SECT283R1', deprecated=True)
|
|
||||||
self._add_curve('sect233r1', 'SECT233R1', deprecated=True)
|
|
||||||
self._add_curve('sect163r2', 'SECT163R2', deprecated=True)
|
|
||||||
|
|
||||||
self.module = module
|
|
||||||
self.cryptography_backend = cryptography.hazmat.backends.default_backend()
|
|
||||||
|
|
||||||
self.type = module.params['type']
|
|
||||||
self.curve = module.params['curve']
|
|
||||||
if not CRYPTOGRAPHY_HAS_X25519 and self.type == 'X25519':
|
|
||||||
self.module.fail_json(msg='Your cryptography version does not support X25519')
|
|
||||||
if not CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519':
|
|
||||||
self.module.fail_json(msg='Your cryptography version does not support X25519 serialization')
|
|
||||||
if not CRYPTOGRAPHY_HAS_X448 and self.type == 'X448':
|
|
||||||
self.module.fail_json(msg='Your cryptography version does not support X448')
|
|
||||||
if not CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519':
|
|
||||||
self.module.fail_json(msg='Your cryptography version does not support Ed25519')
|
|
||||||
if not CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448':
|
|
||||||
self.module.fail_json(msg='Your cryptography version does not support Ed448')
|
|
||||||
|
|
||||||
def _get_wanted_format(self):
|
|
||||||
if self.format not in ('auto', 'auto_ignore'):
|
|
||||||
return self.format
|
|
||||||
if self.type in ('X25519', 'X448', 'Ed25519', 'Ed448'):
|
|
||||||
return 'pkcs8'
|
|
||||||
else:
|
|
||||||
return 'pkcs1'
|
|
||||||
|
|
||||||
def _generate_private_key(self):
|
|
||||||
"""(Re-)Generate private key."""
|
|
||||||
try:
|
|
||||||
if self.type == 'RSA':
|
|
||||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key(
|
|
||||||
public_exponent=65537, # OpenSSL always uses this
|
|
||||||
key_size=self.size,
|
|
||||||
backend=self.cryptography_backend
|
|
||||||
)
|
|
||||||
if self.type == 'DSA':
|
|
||||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key(
|
|
||||||
key_size=self.size,
|
|
||||||
backend=self.cryptography_backend
|
|
||||||
)
|
|
||||||
if CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519':
|
|
||||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate()
|
|
||||||
if CRYPTOGRAPHY_HAS_X448 and self.type == 'X448':
|
|
||||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate()
|
|
||||||
if CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519':
|
|
||||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate()
|
|
||||||
if CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448':
|
|
||||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.generate()
|
|
||||||
if self.type == 'ECC' and self.curve in self.curves:
|
|
||||||
if self.curves[self.curve]['deprecated']:
|
|
||||||
self.module.warn('Elliptic curves of type {0} should not be used for new keys!'.format(self.curve))
|
|
||||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(
|
|
||||||
curve=self.curves[self.curve]['create'](self.size),
|
|
||||||
backend=self.cryptography_backend
|
|
||||||
)
|
|
||||||
except cryptography.exceptions.UnsupportedAlgorithm as dummy:
|
|
||||||
self.module.fail_json(msg='Cryptography backend does not support the algorithm required for {0}'.format(self.type))
|
|
||||||
|
|
||||||
def _ensure_private_key_loaded(self):
|
|
||||||
"""Make sure that the private key has been loaded."""
|
|
||||||
if self.privatekey is None:
|
|
||||||
self.privatekey = self._load_privatekey()
|
|
||||||
|
|
||||||
def _get_private_key_data(self):
|
|
||||||
"""Return bytes for self.privatekey"""
|
|
||||||
# Select export format and encoding
|
|
||||||
try:
|
|
||||||
export_format = self._get_wanted_format()
|
|
||||||
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM
|
|
||||||
if export_format == 'pkcs1':
|
|
||||||
# "TraditionalOpenSSL" format is PKCS1
|
|
||||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL
|
|
||||||
elif export_format == 'pkcs8':
|
|
||||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
|
|
||||||
elif export_format == 'raw':
|
|
||||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.Raw
|
|
||||||
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.Raw
|
|
||||||
except AttributeError:
|
|
||||||
self.module.fail_json(msg='Cryptography backend does not support the selected output format "{0}"'.format(self.format))
|
|
||||||
|
|
||||||
# Select key encryption
|
|
||||||
encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption()
|
|
||||||
if self.cipher and self.passphrase:
|
|
||||||
if self.cipher == 'auto':
|
|
||||||
encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(to_bytes(self.passphrase))
|
|
||||||
else:
|
|
||||||
self.module.fail_json(msg='Cryptography backend can only use "auto" for cipher option.')
|
|
||||||
|
|
||||||
# Serialize key
|
|
||||||
try:
|
|
||||||
return self.privatekey.private_bytes(
|
|
||||||
encoding=export_encoding,
|
|
||||||
format=export_format,
|
|
||||||
encryption_algorithm=encryption_algorithm
|
|
||||||
)
|
|
||||||
except ValueError as dummy:
|
|
||||||
self.module.fail_json(
|
|
||||||
msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(self.format)
|
|
||||||
)
|
|
||||||
except Exception as dummy:
|
|
||||||
self.module.fail_json(
|
|
||||||
msg='Error while serializing the private key in the required format "{0}"'.format(self.format),
|
|
||||||
exception=traceback.format_exc()
|
|
||||||
)
|
|
||||||
|
|
||||||
def _load_privatekey(self):
|
|
||||||
try:
|
|
||||||
# Read bytes
|
|
||||||
with open(self.path, 'rb') as f:
|
|
||||||
data = f.read()
|
|
||||||
# Interpret bytes depending on format.
|
|
||||||
format = crypto_utils.identify_private_key_format(data)
|
|
||||||
if format == 'raw':
|
|
||||||
if len(data) == 56 and CRYPTOGRAPHY_HAS_X448:
|
|
||||||
return cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data)
|
|
||||||
if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448:
|
|
||||||
return cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data)
|
|
||||||
if len(data) == 32:
|
|
||||||
if CRYPTOGRAPHY_HAS_X25519 and (self.type == 'X25519' or not CRYPTOGRAPHY_HAS_ED25519):
|
|
||||||
return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
|
|
||||||
if CRYPTOGRAPHY_HAS_ED25519 and (self.type == 'Ed25519' or not CRYPTOGRAPHY_HAS_X25519):
|
|
||||||
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
|
|
||||||
if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519:
|
|
||||||
try:
|
|
||||||
return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
|
|
||||||
except Exception:
|
|
||||||
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
|
|
||||||
raise PrivateKeyError('Cannot load raw key')
|
|
||||||
else:
|
|
||||||
return cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
|
||||||
data,
|
|
||||||
None if self.passphrase is None else to_bytes(self.passphrase),
|
|
||||||
backend=self.cryptography_backend
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise PrivateKeyError(e)
|
|
||||||
|
|
||||||
def _get_fingerprint(self):
|
|
||||||
# Get bytes of public key
|
|
||||||
private_key = self._load_privatekey()
|
|
||||||
public_key = private_key.public_key()
|
|
||||||
public_key_bytes = public_key.public_bytes(
|
|
||||||
cryptography.hazmat.primitives.serialization.Encoding.DER,
|
|
||||||
cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo
|
|
||||||
)
|
|
||||||
# Get fingerprints of public_key_bytes
|
|
||||||
return crypto_utils.get_fingerprint_of_bytes(public_key_bytes)
|
|
||||||
|
|
||||||
def _check_passphrase(self):
|
|
||||||
try:
|
|
||||||
with open(self.path, 'rb') as f:
|
|
||||||
data = f.read()
|
|
||||||
format = crypto_utils.identify_private_key_format(data)
|
|
||||||
if format == 'raw':
|
|
||||||
# Raw keys cannot be encrypted. To avoid incompatibilities, we try to
|
|
||||||
# actually load the key (and return False when this fails).
|
|
||||||
self._load_privatekey()
|
|
||||||
# Loading the key succeeded. Only return True when no passphrase was
|
|
||||||
# provided.
|
|
||||||
return self.passphrase is None
|
|
||||||
else:
|
|
||||||
return cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
|
||||||
data,
|
|
||||||
None if self.passphrase is None else to_bytes(self.passphrase),
|
|
||||||
backend=self.cryptography_backend
|
|
||||||
)
|
|
||||||
except Exception as dummy:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _check_size_and_type(self):
|
|
||||||
self._ensure_private_key_loaded()
|
|
||||||
|
|
||||||
if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
|
||||||
return self.type == 'RSA' and self.size == self.privatekey.key_size
|
|
||||||
if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
|
||||||
return self.type == 'DSA' and self.size == self.privatekey.key_size
|
|
||||||
if CRYPTOGRAPHY_HAS_X25519 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey):
|
|
||||||
return self.type == 'X25519'
|
|
||||||
if CRYPTOGRAPHY_HAS_X448 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey):
|
|
||||||
return self.type == 'X448'
|
|
||||||
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
|
||||||
return self.type == 'Ed25519'
|
|
||||||
if CRYPTOGRAPHY_HAS_ED448 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
|
||||||
return self.type == 'Ed448'
|
|
||||||
if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
|
||||||
if self.type != 'ECC':
|
|
||||||
return False
|
|
||||||
if self.curve not in self.curves:
|
|
||||||
return False
|
|
||||||
return self.curves[self.curve]['verify'](self.privatekey)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _check_format(self):
|
|
||||||
if self.format == 'auto_ignore':
|
|
||||||
return True
|
|
||||||
try:
|
|
||||||
with open(self.path, 'rb') as f:
|
|
||||||
content = f.read()
|
|
||||||
format = crypto_utils.identify_private_key_format(content)
|
|
||||||
return format == self._get_wanted_format()
|
|
||||||
except Exception as dummy:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
"""Serialize the object into a dictionary."""
|
|
||||||
result = super(PrivateKeyCryptography, self).dump()
|
|
||||||
result['type'] = self.type
|
|
||||||
if self.type == 'ECC':
|
|
||||||
result['curve'] = self.curve
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
|
||||||
size=dict(type='int', default=4096),
|
|
||||||
type=dict(type='str', default='RSA', choices=[
|
|
||||||
'DSA', 'ECC', 'Ed25519', 'Ed448', 'RSA', 'X25519', 'X448'
|
|
||||||
]),
|
|
||||||
curve=dict(type='str', choices=[
|
|
||||||
'secp384r1', 'secp521r1', 'secp224r1', 'secp192r1', 'secp256r1',
|
|
||||||
'secp256k1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1',
|
|
||||||
'sect571k1', 'sect409k1', 'sect283k1', 'sect233k1', 'sect163k1',
|
|
||||||
'sect571r1', 'sect409r1', 'sect283r1', 'sect233r1', 'sect163r2',
|
|
||||||
]),
|
|
||||||
force=dict(type='bool', default=False),
|
|
||||||
path=dict(type='path', required=True),
|
|
||||||
passphrase=dict(type='str', no_log=True),
|
|
||||||
cipher=dict(type='str'),
|
|
||||||
backup=dict(type='bool', default=False),
|
|
||||||
format=dict(type='str', default='auto_ignore', choices=['pkcs1', 'pkcs8', 'raw', 'auto', 'auto_ignore']),
|
|
||||||
format_mismatch=dict(type='str', default='regenerate', choices=['regenerate', 'convert']),
|
|
||||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
|
||||||
return_content=dict(type='bool', default=False),
|
|
||||||
regenerate=dict(
|
|
||||||
type='str',
|
|
||||||
default='full_idempotence',
|
|
||||||
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
|
|
||||||
),
|
|
||||||
),
|
|
||||||
supports_check_mode=True,
|
|
||||||
add_file_common_args=True,
|
|
||||||
required_together=[
|
|
||||||
['cipher', 'passphrase']
|
|
||||||
],
|
|
||||||
required_if=[
|
|
||||||
['type', 'ECC', ['curve']],
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
|
||||||
if not os.path.isdir(base_dir):
|
|
||||||
module.fail_json(
|
|
||||||
name=base_dir,
|
|
||||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
backend = module.params['select_crypto_backend']
|
|
||||||
if backend == 'auto':
|
|
||||||
# Detection what is possible
|
|
||||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
|
||||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
|
||||||
|
|
||||||
# Decision
|
|
||||||
if module.params['cipher'] and module.params['passphrase'] and module.params['cipher'] != 'auto':
|
|
||||||
# First try pyOpenSSL, then cryptography
|
|
||||||
if can_use_pyopenssl:
|
|
||||||
backend = 'pyopenssl'
|
|
||||||
elif can_use_cryptography:
|
|
||||||
backend = 'cryptography'
|
|
||||||
else:
|
|
||||||
# First try cryptography, then pyOpenSSL
|
|
||||||
if can_use_cryptography:
|
|
||||||
backend = 'cryptography'
|
|
||||||
elif can_use_pyopenssl:
|
|
||||||
backend = 'pyopenssl'
|
|
||||||
|
|
||||||
# Success?
|
|
||||||
if backend == 'auto':
|
|
||||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
|
||||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
|
||||||
MINIMAL_PYOPENSSL_VERSION))
|
|
||||||
try:
|
|
||||||
if backend == 'pyopenssl':
|
|
||||||
if not PYOPENSSL_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
|
||||||
exception=PYOPENSSL_IMP_ERR)
|
|
||||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13')
|
|
||||||
private_key = PrivateKeyPyOpenSSL(module)
|
|
||||||
elif backend == 'cryptography':
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
private_key = PrivateKeyCryptography(module)
|
|
||||||
|
|
||||||
if private_key.state == 'present':
|
|
||||||
if module.check_mode:
|
|
||||||
result = private_key.dump()
|
|
||||||
result['changed'] = private_key.force \
|
|
||||||
or not private_key.check(module, ignore_conversion=True) \
|
|
||||||
or not private_key.check(module, ignore_conversion=False)
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
private_key.generate(module)
|
|
||||||
else:
|
|
||||||
if module.check_mode:
|
|
||||||
result = private_key.dump()
|
|
||||||
result['changed'] = os.path.exists(module.params['path'])
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
private_key.remove(module)
|
|
||||||
|
|
||||||
result = private_key.dump()
|
|
||||||
module.exit_json(**result)
|
|
||||||
except crypto_utils.OpenSSLObjectError as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,651 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
|
||||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
---
|
|
||||||
module: openssl_privatekey_info
|
|
||||||
version_added: '2.8'
|
|
||||||
short_description: Provide information for OpenSSL private keys
|
|
||||||
description:
|
|
||||||
- This module allows one to query information on OpenSSL private keys.
|
|
||||||
- In case the key consistency checks fail, the module will fail as this indicates a faked
|
|
||||||
private key. In this case, all return variables are still returned. Note that key consistency
|
|
||||||
checks are not available all key types; if none is available, C(none) is returned for
|
|
||||||
C(key_is_consistent).
|
|
||||||
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
|
|
||||||
cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
|
|
||||||
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
|
|
||||||
C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
|
|
||||||
and will be removed in Ansible 2.13.
|
|
||||||
requirements:
|
|
||||||
- PyOpenSSL >= 0.15 or cryptography >= 1.2.3
|
|
||||||
author:
|
|
||||||
- Felix Fontein (@felixfontein)
|
|
||||||
- Yanis Guenane (@Spredzy)
|
|
||||||
options:
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- Remote absolute path where the private key file is loaded from.
|
|
||||||
type: path
|
|
||||||
content:
|
|
||||||
description:
|
|
||||||
- Content of the private key file.
|
|
||||||
- Either I(path) or I(content) must be specified, but not both.
|
|
||||||
type: str
|
|
||||||
version_added: "2.10"
|
|
||||||
passphrase:
|
|
||||||
description:
|
|
||||||
- The passphrase for the private key.
|
|
||||||
type: str
|
|
||||||
return_private_key_data:
|
|
||||||
description:
|
|
||||||
- Whether to return private key data.
|
|
||||||
- Only set this to C(yes) when you want private information about this key to
|
|
||||||
leave the remote machine.
|
|
||||||
- "WARNING: you have to make sure that private key data isn't accidentally logged!"
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
|
|
||||||
select_crypto_backend:
|
|
||||||
description:
|
|
||||||
- Determines which crypto backend to use.
|
|
||||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
|
||||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
|
||||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
|
||||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13.
|
|
||||||
From that point on, only the C(cryptography) backend will be available.
|
|
||||||
type: str
|
|
||||||
default: auto
|
|
||||||
choices: [ auto, cryptography, pyopenssl ]
|
|
||||||
|
|
||||||
seealso:
|
|
||||||
- module: openssl_privatekey
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Generate an OpenSSL private key with the default values (4096 bits, RSA)
|
|
||||||
openssl_privatekey:
|
|
||||||
path: /etc/ssl/private/ansible.com.pem
|
|
||||||
|
|
||||||
- name: Get information on generated key
|
|
||||||
openssl_privatekey_info:
|
|
||||||
path: /etc/ssl/private/ansible.com.pem
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- name: Dump information
|
|
||||||
debug:
|
|
||||||
var: result
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = r'''
|
|
||||||
can_load_key:
|
|
||||||
description: Whether the module was able to load the private key from disk
|
|
||||||
returned: always
|
|
||||||
type: bool
|
|
||||||
can_parse_key:
|
|
||||||
description: Whether the module was able to parse the private key
|
|
||||||
returned: always
|
|
||||||
type: bool
|
|
||||||
key_is_consistent:
|
|
||||||
description:
|
|
||||||
- Whether the key is consistent. Can also return C(none) next to C(yes) and
|
|
||||||
C(no), to indicate that consistency couldn't be checked.
|
|
||||||
- In case the check returns C(no), the module will fail.
|
|
||||||
returned: always
|
|
||||||
type: bool
|
|
||||||
public_key:
|
|
||||||
description: Private key's public key in PEM format
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
|
||||||
public_key_fingerprints:
|
|
||||||
description:
|
|
||||||
- Fingerprints of private key's public key.
|
|
||||||
- For every hash algorithm available, the fingerprint is computed.
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
|
|
||||||
'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
|
|
||||||
type:
|
|
||||||
description:
|
|
||||||
- The key's type.
|
|
||||||
- One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
|
|
||||||
- Will start with C(unknown) if the key type cannot be determined.
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: RSA
|
|
||||||
public_data:
|
|
||||||
description:
|
|
||||||
- Public key data. Depends on key type.
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
private_data:
|
|
||||||
description:
|
|
||||||
- Private key data. Depends on key type.
|
|
||||||
returned: success and when I(return_private_key_data) is set to C(yes)
|
|
||||||
type: dict
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
|
|
||||||
from ansible.module_utils import crypto as crypto_utils
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
from ansible.module_utils._text import to_native, to_bytes
|
|
||||||
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
|
||||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
|
||||||
|
|
||||||
PYOPENSSL_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import OpenSSL
|
|
||||||
from OpenSSL import crypto
|
|
||||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
|
||||||
except ImportError:
|
|
||||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
|
||||||
PYOPENSSL_FOUND = False
|
|
||||||
else:
|
|
||||||
PYOPENSSL_FOUND = True
|
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
try:
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.x25519
|
|
||||||
CRYPTOGRAPHY_HAS_X25519 = True
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_HAS_X25519 = False
|
|
||||||
try:
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.x448
|
|
||||||
CRYPTOGRAPHY_HAS_X448 = True
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_HAS_X448 = False
|
|
||||||
try:
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.ed25519
|
|
||||||
CRYPTOGRAPHY_HAS_ED25519 = True
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_HAS_ED25519 = False
|
|
||||||
try:
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.ed448
|
|
||||||
CRYPTOGRAPHY_HAS_ED448 = True
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_HAS_ED448 = False
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
SIGNATURE_TEST_DATA = b'1234'
|
|
||||||
|
|
||||||
|
|
||||||
def _get_cryptography_key_info(key):
|
|
||||||
key_public_data = dict()
|
|
||||||
key_private_data = dict()
|
|
||||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
|
||||||
key_type = 'RSA'
|
|
||||||
key_public_data['size'] = key.key_size
|
|
||||||
key_public_data['modulus'] = key.public_key().public_numbers().n
|
|
||||||
key_public_data['exponent'] = key.public_key().public_numbers().e
|
|
||||||
key_private_data['p'] = key.private_numbers().p
|
|
||||||
key_private_data['q'] = key.private_numbers().q
|
|
||||||
key_private_data['exponent'] = key.private_numbers().d
|
|
||||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
|
||||||
key_type = 'DSA'
|
|
||||||
key_public_data['size'] = key.key_size
|
|
||||||
key_public_data['p'] = key.parameters().parameter_numbers().p
|
|
||||||
key_public_data['q'] = key.parameters().parameter_numbers().q
|
|
||||||
key_public_data['g'] = key.parameters().parameter_numbers().g
|
|
||||||
key_public_data['y'] = key.public_key().public_numbers().y
|
|
||||||
key_private_data['x'] = key.private_numbers().x
|
|
||||||
elif CRYPTOGRAPHY_HAS_X25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey):
|
|
||||||
key_type = 'X25519'
|
|
||||||
elif CRYPTOGRAPHY_HAS_X448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey):
|
|
||||||
key_type = 'X448'
|
|
||||||
elif CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
|
||||||
key_type = 'Ed25519'
|
|
||||||
elif CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
|
||||||
key_type = 'Ed448'
|
|
||||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
|
||||||
key_type = 'ECC'
|
|
||||||
key_public_data['curve'] = key.public_key().curve.name
|
|
||||||
key_public_data['x'] = key.public_key().public_numbers().x
|
|
||||||
key_public_data['y'] = key.public_key().public_numbers().y
|
|
||||||
key_public_data['exponent_size'] = key.public_key().curve.key_size
|
|
||||||
key_private_data['multiplier'] = key.private_numbers().private_value
|
|
||||||
else:
|
|
||||||
key_type = 'unknown ({0})'.format(type(key))
|
|
||||||
return key_type, key_public_data, key_private_data
|
|
||||||
|
|
||||||
|
|
||||||
def _check_dsa_consistency(key_public_data, key_private_data):
|
|
||||||
# Get parameters
|
|
||||||
p = key_public_data.get('p')
|
|
||||||
q = key_public_data.get('q')
|
|
||||||
g = key_public_data.get('g')
|
|
||||||
y = key_public_data.get('y')
|
|
||||||
x = key_private_data.get('x')
|
|
||||||
for v in (p, q, g, y, x):
|
|
||||||
if v is None:
|
|
||||||
return None
|
|
||||||
# Make sure that g is not 0, 1 or -1 in Z/pZ
|
|
||||||
if g < 2 or g >= p - 1:
|
|
||||||
return False
|
|
||||||
# Make sure that x is in range
|
|
||||||
if x < 1 or x >= q:
|
|
||||||
return False
|
|
||||||
# Check whether q divides p-1
|
|
||||||
if (p - 1) % q != 0:
|
|
||||||
return False
|
|
||||||
# Check that g**q mod p == 1
|
|
||||||
if crypto_utils.binary_exp_mod(g, q, p) != 1:
|
|
||||||
return False
|
|
||||||
# Check whether g**x mod p == y
|
|
||||||
if crypto_utils.binary_exp_mod(g, x, p) != y:
|
|
||||||
return False
|
|
||||||
# Check (quickly) whether p or q are not primes
|
|
||||||
if crypto_utils.quick_is_not_prime(q) or crypto_utils.quick_is_not_prime(p):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _is_cryptography_key_consistent(key, key_public_data, key_private_data):
|
|
||||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
|
||||||
return bool(key._backend._lib.RSA_check_key(key._rsa_cdata))
|
|
||||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
|
||||||
result = _check_dsa_consistency(key_public_data, key_private_data)
|
|
||||||
if result is not None:
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
signature = key.sign(SIGNATURE_TEST_DATA, cryptography.hazmat.primitives.hashes.SHA256())
|
|
||||||
except AttributeError:
|
|
||||||
# sign() was added in cryptography 1.5, but we support older versions
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
key.public_key().verify(
|
|
||||||
signature,
|
|
||||||
SIGNATURE_TEST_DATA,
|
|
||||||
cryptography.hazmat.primitives.hashes.SHA256()
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
except cryptography.exceptions.InvalidSignature:
|
|
||||||
return False
|
|
||||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
|
||||||
try:
|
|
||||||
signature = key.sign(
|
|
||||||
SIGNATURE_TEST_DATA,
|
|
||||||
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256())
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
# sign() was added in cryptography 1.5, but we support older versions
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
key.public_key().verify(
|
|
||||||
signature,
|
|
||||||
SIGNATURE_TEST_DATA,
|
|
||||||
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256())
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
except cryptography.exceptions.InvalidSignature:
|
|
||||||
return False
|
|
||||||
has_simple_sign_function = False
|
|
||||||
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
|
||||||
has_simple_sign_function = True
|
|
||||||
if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
|
||||||
has_simple_sign_function = True
|
|
||||||
if has_simple_sign_function:
|
|
||||||
signature = key.sign(SIGNATURE_TEST_DATA)
|
|
||||||
try:
|
|
||||||
key.public_key().verify(signature, SIGNATURE_TEST_DATA)
|
|
||||||
return True
|
|
||||||
except cryptography.exceptions.InvalidSignature:
|
|
||||||
return False
|
|
||||||
# For X25519 and X448, there's no test yet.
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class PrivateKeyInfo(crypto_utils.OpenSSLObject):
|
|
||||||
def __init__(self, module, backend):
|
|
||||||
super(PrivateKeyInfo, self).__init__(
|
|
||||||
module.params['path'] or '',
|
|
||||||
'present',
|
|
||||||
False,
|
|
||||||
module.check_mode,
|
|
||||||
)
|
|
||||||
self.backend = backend
|
|
||||||
self.module = module
|
|
||||||
self.content = module.params['content']
|
|
||||||
|
|
||||||
self.passphrase = module.params['passphrase']
|
|
||||||
self.return_private_key_data = module.params['return_private_key_data']
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
# Empty method because crypto_utils.OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
# Empty method because crypto_utils.OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_key_info(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _is_key_consistent(self, key_public_data, key_private_data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_info(self):
|
|
||||||
result = dict(
|
|
||||||
can_load_key=False,
|
|
||||||
can_parse_key=False,
|
|
||||||
key_is_consistent=None,
|
|
||||||
)
|
|
||||||
if self.content is not None:
|
|
||||||
priv_key_detail = self.content.encode('utf-8')
|
|
||||||
result['can_load_key'] = True
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
with open(self.path, 'rb') as b_priv_key_fh:
|
|
||||||
priv_key_detail = b_priv_key_fh.read()
|
|
||||||
result['can_load_key'] = True
|
|
||||||
except (IOError, OSError) as exc:
|
|
||||||
self.module.fail_json(msg=to_native(exc), **result)
|
|
||||||
try:
|
|
||||||
self.key = crypto_utils.load_privatekey(
|
|
||||||
path=None,
|
|
||||||
content=priv_key_detail,
|
|
||||||
passphrase=to_bytes(self.passphrase) if self.passphrase is not None else self.passphrase,
|
|
||||||
backend=self.backend
|
|
||||||
)
|
|
||||||
result['can_parse_key'] = True
|
|
||||||
except crypto_utils.OpenSSLObjectError as exc:
|
|
||||||
self.module.fail_json(msg=to_native(exc), **result)
|
|
||||||
|
|
||||||
result['public_key'] = self._get_public_key(binary=False)
|
|
||||||
pk = self._get_public_key(binary=True)
|
|
||||||
result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict()
|
|
||||||
|
|
||||||
key_type, key_public_data, key_private_data = self._get_key_info()
|
|
||||||
result['type'] = key_type
|
|
||||||
result['public_data'] = key_public_data
|
|
||||||
if self.return_private_key_data:
|
|
||||||
result['private_data'] = key_private_data
|
|
||||||
|
|
||||||
result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data)
|
|
||||||
if result['key_is_consistent'] is False:
|
|
||||||
# Only fail when it is False, to avoid to fail on None (which means "we don't know")
|
|
||||||
result['key_is_consistent'] = False
|
|
||||||
self.module.fail_json(
|
|
||||||
msg="Private key is not consistent! (See "
|
|
||||||
"https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)",
|
|
||||||
**result
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class PrivateKeyInfoCryptography(PrivateKeyInfo):
|
|
||||||
"""Validate the supplied private key, using the cryptography backend"""
|
|
||||||
def __init__(self, module):
|
|
||||||
super(PrivateKeyInfoCryptography, self).__init__(module, 'cryptography')
|
|
||||||
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
return self.key.public_key().public_bytes(
|
|
||||||
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
|
||||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_key_info(self):
|
|
||||||
return _get_cryptography_key_info(self.key)
|
|
||||||
|
|
||||||
def _is_key_consistent(self, key_public_data, key_private_data):
|
|
||||||
return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data)
|
|
||||||
|
|
||||||
|
|
||||||
class PrivateKeyInfoPyOpenSSL(PrivateKeyInfo):
|
|
||||||
"""validate the supplied private key."""
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(PrivateKeyInfoPyOpenSSL, self).__init__(module, 'pyopenssl')
|
|
||||||
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
try:
|
|
||||||
return crypto.dump_publickey(
|
|
||||||
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
|
|
||||||
self.key
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
# pyOpenSSL < 16.0:
|
|
||||||
bio = crypto._new_mem_buf()
|
|
||||||
if binary:
|
|
||||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.key._pkey)
|
|
||||||
else:
|
|
||||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.key._pkey)
|
|
||||||
if rc != 1:
|
|
||||||
crypto._raise_current_error()
|
|
||||||
return crypto._bio_to_string(bio)
|
|
||||||
except AttributeError:
|
|
||||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
|
||||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
|
||||||
|
|
||||||
def bigint_to_int(self, bn):
|
|
||||||
'''Convert OpenSSL BIGINT to Python integer'''
|
|
||||||
if bn == OpenSSL._util.ffi.NULL:
|
|
||||||
return None
|
|
||||||
hexstr = OpenSSL._util.lib.BN_bn2hex(bn)
|
|
||||||
try:
|
|
||||||
return int(OpenSSL._util.ffi.string(hexstr), 16)
|
|
||||||
finally:
|
|
||||||
OpenSSL._util.lib.OPENSSL_free(hexstr)
|
|
||||||
|
|
||||||
def _get_key_info(self):
|
|
||||||
key_public_data = dict()
|
|
||||||
key_private_data = dict()
|
|
||||||
openssl_key_type = self.key.type()
|
|
||||||
try_fallback = True
|
|
||||||
if crypto.TYPE_RSA == openssl_key_type:
|
|
||||||
key_type = 'RSA'
|
|
||||||
key_public_data['size'] = self.key.bits()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Use OpenSSL directly to extract key data
|
|
||||||
key = OpenSSL._util.lib.EVP_PKEY_get1_RSA(self.key._pkey)
|
|
||||||
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.RSA_free)
|
|
||||||
# OpenSSL 1.1 and newer have functions to extract the parameters
|
|
||||||
# from the EVP PKEY data structures. Older versions didn't have
|
|
||||||
# these getters, and it was common use to simply access the values
|
|
||||||
# directly. Since there's no guarantee that these data structures
|
|
||||||
# will still be accessible in the future, we use the getters for
|
|
||||||
# 1.1 and later, and directly access the values for 1.0.x and
|
|
||||||
# earlier.
|
|
||||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
|
||||||
# Get modulus and exponents
|
|
||||||
n = OpenSSL._util.ffi.new("BIGNUM **")
|
|
||||||
e = OpenSSL._util.ffi.new("BIGNUM **")
|
|
||||||
d = OpenSSL._util.ffi.new("BIGNUM **")
|
|
||||||
OpenSSL._util.lib.RSA_get0_key(key, n, e, d)
|
|
||||||
key_public_data['modulus'] = self.bigint_to_int(n[0])
|
|
||||||
key_public_data['exponent'] = self.bigint_to_int(e[0])
|
|
||||||
key_private_data['exponent'] = self.bigint_to_int(d[0])
|
|
||||||
# Get factors
|
|
||||||
p = OpenSSL._util.ffi.new("BIGNUM **")
|
|
||||||
q = OpenSSL._util.ffi.new("BIGNUM **")
|
|
||||||
OpenSSL._util.lib.RSA_get0_factors(key, p, q)
|
|
||||||
key_private_data['p'] = self.bigint_to_int(p[0])
|
|
||||||
key_private_data['q'] = self.bigint_to_int(q[0])
|
|
||||||
else:
|
|
||||||
# Get modulus and exponents
|
|
||||||
key_public_data['modulus'] = self.bigint_to_int(key.n)
|
|
||||||
key_public_data['exponent'] = self.bigint_to_int(key.e)
|
|
||||||
key_private_data['exponent'] = self.bigint_to_int(key.d)
|
|
||||||
# Get factors
|
|
||||||
key_private_data['p'] = self.bigint_to_int(key.p)
|
|
||||||
key_private_data['q'] = self.bigint_to_int(key.q)
|
|
||||||
try_fallback = False
|
|
||||||
except AttributeError:
|
|
||||||
# Use fallback if available
|
|
||||||
pass
|
|
||||||
elif crypto.TYPE_DSA == openssl_key_type:
|
|
||||||
key_type = 'DSA'
|
|
||||||
key_public_data['size'] = self.key.bits()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Use OpenSSL directly to extract key data
|
|
||||||
key = OpenSSL._util.lib.EVP_PKEY_get1_DSA(self.key._pkey)
|
|
||||||
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.DSA_free)
|
|
||||||
# OpenSSL 1.1 and newer have functions to extract the parameters
|
|
||||||
# from the EVP PKEY data structures. Older versions didn't have
|
|
||||||
# these getters, and it was common use to simply access the values
|
|
||||||
# directly. Since there's no guarantee that these data structures
|
|
||||||
# will still be accessible in the future, we use the getters for
|
|
||||||
# 1.1 and later, and directly access the values for 1.0.x and
|
|
||||||
# earlier.
|
|
||||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
|
||||||
# Get public parameters (primes and group element)
|
|
||||||
p = OpenSSL._util.ffi.new("BIGNUM **")
|
|
||||||
q = OpenSSL._util.ffi.new("BIGNUM **")
|
|
||||||
g = OpenSSL._util.ffi.new("BIGNUM **")
|
|
||||||
OpenSSL._util.lib.DSA_get0_pqg(key, p, q, g)
|
|
||||||
key_public_data['p'] = self.bigint_to_int(p[0])
|
|
||||||
key_public_data['q'] = self.bigint_to_int(q[0])
|
|
||||||
key_public_data['g'] = self.bigint_to_int(g[0])
|
|
||||||
# Get public and private key exponents
|
|
||||||
y = OpenSSL._util.ffi.new("BIGNUM **")
|
|
||||||
x = OpenSSL._util.ffi.new("BIGNUM **")
|
|
||||||
OpenSSL._util.lib.DSA_get0_key(key, y, x)
|
|
||||||
key_public_data['y'] = self.bigint_to_int(y[0])
|
|
||||||
key_private_data['x'] = self.bigint_to_int(x[0])
|
|
||||||
else:
|
|
||||||
# Get public parameters (primes and group element)
|
|
||||||
key_public_data['p'] = self.bigint_to_int(key.p)
|
|
||||||
key_public_data['q'] = self.bigint_to_int(key.q)
|
|
||||||
key_public_data['g'] = self.bigint_to_int(key.g)
|
|
||||||
# Get public and private key exponents
|
|
||||||
key_public_data['y'] = self.bigint_to_int(key.pub_key)
|
|
||||||
key_private_data['x'] = self.bigint_to_int(key.priv_key)
|
|
||||||
try_fallback = False
|
|
||||||
except AttributeError:
|
|
||||||
# Use fallback if available
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# Return 'unknown'
|
|
||||||
key_type = 'unknown ({0})'.format(self.key.type())
|
|
||||||
# If needed and if possible, fall back to cryptography
|
|
||||||
if try_fallback and PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
|
|
||||||
return _get_cryptography_key_info(self.key.to_cryptography_key())
|
|
||||||
return key_type, key_public_data, key_private_data
|
|
||||||
|
|
||||||
def _is_key_consistent(self, key_public_data, key_private_data):
|
|
||||||
openssl_key_type = self.key.type()
|
|
||||||
if crypto.TYPE_RSA == openssl_key_type:
|
|
||||||
try:
|
|
||||||
return self.key.check()
|
|
||||||
except crypto.Error:
|
|
||||||
# OpenSSL error means that key is not consistent
|
|
||||||
return False
|
|
||||||
if crypto.TYPE_DSA == openssl_key_type:
|
|
||||||
result = _check_dsa_consistency(key_public_data, key_private_data)
|
|
||||||
if result is not None:
|
|
||||||
return result
|
|
||||||
signature = crypto.sign(self.key, SIGNATURE_TEST_DATA, 'sha256')
|
|
||||||
# Verify wants a cert (where it can get the public key from)
|
|
||||||
cert = crypto.X509()
|
|
||||||
cert.set_pubkey(self.key)
|
|
||||||
try:
|
|
||||||
crypto.verify(cert, signature, SIGNATURE_TEST_DATA, 'sha256')
|
|
||||||
return True
|
|
||||||
except crypto.Error:
|
|
||||||
return False
|
|
||||||
# If needed and if possible, fall back to cryptography
|
|
||||||
if PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
|
|
||||||
return _is_cryptography_key_consistent(self.key.to_cryptography_key(), key_public_data, key_private_data)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
path=dict(type='path'),
|
|
||||||
content=dict(type='str'),
|
|
||||||
passphrase=dict(type='str', no_log=True),
|
|
||||||
return_private_key_data=dict(type='bool', default=False),
|
|
||||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
|
||||||
),
|
|
||||||
required_one_of=(
|
|
||||||
['path', 'content'],
|
|
||||||
),
|
|
||||||
mutually_exclusive=(
|
|
||||||
['path', 'content'],
|
|
||||||
),
|
|
||||||
supports_check_mode=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if module.params['path'] is not None:
|
|
||||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
|
||||||
if not os.path.isdir(base_dir):
|
|
||||||
module.fail_json(
|
|
||||||
name=base_dir,
|
|
||||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
backend = module.params['select_crypto_backend']
|
|
||||||
if backend == 'auto':
|
|
||||||
# Detect what backend we can use
|
|
||||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
|
||||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
|
||||||
|
|
||||||
# If cryptography is available we'll use it
|
|
||||||
if can_use_cryptography:
|
|
||||||
backend = 'cryptography'
|
|
||||||
elif can_use_pyopenssl:
|
|
||||||
backend = 'pyopenssl'
|
|
||||||
|
|
||||||
# Fail if no backend has been found
|
|
||||||
if backend == 'auto':
|
|
||||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
|
||||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
|
||||||
MINIMAL_PYOPENSSL_VERSION))
|
|
||||||
|
|
||||||
if backend == 'pyopenssl':
|
|
||||||
if not PYOPENSSL_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
|
||||||
exception=PYOPENSSL_IMP_ERR)
|
|
||||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13')
|
|
||||||
privatekey = PrivateKeyInfoPyOpenSSL(module)
|
|
||||||
elif backend == 'cryptography':
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
privatekey = PrivateKeyInfoCryptography(module)
|
|
||||||
|
|
||||||
result = privatekey.get_info()
|
|
||||||
module.exit_json(**result)
|
|
||||||
except crypto_utils.OpenSSLObjectError as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
@ -1,478 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright: (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
---
|
|
||||||
module: openssl_publickey
|
|
||||||
version_added: "2.3"
|
|
||||||
short_description: Generate an OpenSSL public key from its private key.
|
|
||||||
description:
|
|
||||||
- This module allows one to (re)generate OpenSSL public keys from their private keys.
|
|
||||||
- Keys are generated in PEM or OpenSSH format.
|
|
||||||
- The module can use the cryptography Python library, or the pyOpenSSL Python
|
|
||||||
library. By default, it tries to detect which one is available. This can be
|
|
||||||
overridden with the I(select_crypto_backend) option. When I(format) is C(OpenSSH),
|
|
||||||
the C(cryptography) backend has to be used. Please note that the PyOpenSSL backend
|
|
||||||
was deprecated in Ansible 2.9 and will be removed in Ansible 2.13."
|
|
||||||
requirements:
|
|
||||||
- Either cryptography >= 1.2.3 (older versions might work as well)
|
|
||||||
- Or pyOpenSSL >= 16.0.0
|
|
||||||
- Needs cryptography >= 1.4 if I(format) is C(OpenSSH)
|
|
||||||
author:
|
|
||||||
- Yanis Guenane (@Spredzy)
|
|
||||||
- Felix Fontein (@felixfontein)
|
|
||||||
options:
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- Whether the public key should exist or not, taking action if the state is different from what is stated.
|
|
||||||
type: str
|
|
||||||
default: present
|
|
||||||
choices: [ absent, present ]
|
|
||||||
force:
|
|
||||||
description:
|
|
||||||
- Should the key be regenerated even it it already exists.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
format:
|
|
||||||
description:
|
|
||||||
- The format of the public key.
|
|
||||||
type: str
|
|
||||||
default: PEM
|
|
||||||
choices: [ OpenSSH, PEM ]
|
|
||||||
version_added: "2.4"
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- Name of the file in which the generated TLS/SSL public key will be written.
|
|
||||||
type: path
|
|
||||||
required: true
|
|
||||||
privatekey_path:
|
|
||||||
description:
|
|
||||||
- Path to the TLS/SSL private key from which to generate the public key.
|
|
||||||
- Either I(privatekey_path) or I(privatekey_content) must be specified, but not both.
|
|
||||||
If I(state) is C(present), one of them is required.
|
|
||||||
type: path
|
|
||||||
privatekey_content:
|
|
||||||
description:
|
|
||||||
- The content of the TLS/SSL private key from which to generate the public key.
|
|
||||||
- Either I(privatekey_path) or I(privatekey_content) must be specified, but not both.
|
|
||||||
If I(state) is C(present), one of them is required.
|
|
||||||
type: str
|
|
||||||
version_added: "2.10"
|
|
||||||
privatekey_passphrase:
|
|
||||||
description:
|
|
||||||
- The passphrase for the private key.
|
|
||||||
type: str
|
|
||||||
version_added: "2.4"
|
|
||||||
backup:
|
|
||||||
description:
|
|
||||||
- Create a backup file including a timestamp so you can get the original
|
|
||||||
public key back if you overwrote it with a different one by accident.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
version_added: "2.8"
|
|
||||||
select_crypto_backend:
|
|
||||||
description:
|
|
||||||
- Determines which crypto backend to use.
|
|
||||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
|
||||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
|
||||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
|
||||||
type: str
|
|
||||||
default: auto
|
|
||||||
choices: [ auto, cryptography, pyopenssl ]
|
|
||||||
version_added: "2.9"
|
|
||||||
return_content:
|
|
||||||
description:
|
|
||||||
- If set to C(yes), will return the (current or generated) public key's content as I(publickey).
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
version_added: "2.10"
|
|
||||||
extends_documentation_fragment:
|
|
||||||
- files
|
|
||||||
seealso:
|
|
||||||
- module: openssl_certificate
|
|
||||||
- module: openssl_csr
|
|
||||||
- module: openssl_dhparam
|
|
||||||
- module: openssl_pkcs12
|
|
||||||
- module: openssl_privatekey
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Generate an OpenSSL public key in PEM format
|
|
||||||
openssl_publickey:
|
|
||||||
path: /etc/ssl/public/ansible.com.pem
|
|
||||||
privatekey_path: /etc/ssl/private/ansible.com.pem
|
|
||||||
|
|
||||||
- name: Generate an OpenSSL public key in PEM format from an inline key
|
|
||||||
openssl_publickey:
|
|
||||||
path: /etc/ssl/public/ansible.com.pem
|
|
||||||
privatekey_content: "{{ private_key_content }}"
|
|
||||||
|
|
||||||
- name: Generate an OpenSSL public key in OpenSSH v2 format
|
|
||||||
openssl_publickey:
|
|
||||||
path: /etc/ssl/public/ansible.com.pem
|
|
||||||
privatekey_path: /etc/ssl/private/ansible.com.pem
|
|
||||||
format: OpenSSH
|
|
||||||
|
|
||||||
- name: Generate an OpenSSL public key with a passphrase protected private key
|
|
||||||
openssl_publickey:
|
|
||||||
path: /etc/ssl/public/ansible.com.pem
|
|
||||||
privatekey_path: /etc/ssl/private/ansible.com.pem
|
|
||||||
privatekey_passphrase: ansible
|
|
||||||
|
|
||||||
- name: Force regenerate an OpenSSL public key if it already exists
|
|
||||||
openssl_publickey:
|
|
||||||
path: /etc/ssl/public/ansible.com.pem
|
|
||||||
privatekey_path: /etc/ssl/private/ansible.com.pem
|
|
||||||
force: yes
|
|
||||||
|
|
||||||
- name: Remove an OpenSSL public key
|
|
||||||
openssl_publickey:
|
|
||||||
path: /etc/ssl/public/ansible.com.pem
|
|
||||||
state: absent
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = r'''
|
|
||||||
privatekey:
|
|
||||||
description:
|
|
||||||
- Path to the TLS/SSL private key the public key was generated from.
|
|
||||||
- Will be C(none) if the private key has been provided in I(privatekey_content).
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: /etc/ssl/private/ansible.com.pem
|
|
||||||
format:
|
|
||||||
description: The format of the public key (PEM, OpenSSH, ...).
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: PEM
|
|
||||||
filename:
|
|
||||||
description: Path to the generated TLS/SSL public key file.
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: /etc/ssl/public/ansible.com.pem
|
|
||||||
fingerprint:
|
|
||||||
description:
|
|
||||||
- The fingerprint of the public key. Fingerprint will be generated for each hashlib.algorithms available.
|
|
||||||
- Requires PyOpenSSL >= 16.0 for meaningful output.
|
|
||||||
returned: changed or success
|
|
||||||
type: dict
|
|
||||||
sample:
|
|
||||||
md5: "84:75:71:72:8d:04:b5:6c:4d:37:6d:66:83:f5:4c:29"
|
|
||||||
sha1: "51:cc:7c:68:5d:eb:41:43:88:7e:1a:ae:c7:f8:24:72:ee:71:f6:10"
|
|
||||||
sha224: "b1:19:a6:6c:14:ac:33:1d:ed:18:50:d3:06:5c:b2:32:91:f1:f1:52:8c:cb:d5:75:e9:f5:9b:46"
|
|
||||||
sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7"
|
|
||||||
sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d"
|
|
||||||
sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b"
|
|
||||||
backup_file:
|
|
||||||
description: Name of backup file created.
|
|
||||||
returned: changed and if I(backup) is C(yes)
|
|
||||||
type: str
|
|
||||||
sample: /path/to/publickey.pem.2019-03-09@11:22~
|
|
||||||
publickey:
|
|
||||||
description: The (current or generated) public key's content.
|
|
||||||
returned: if I(state) is C(present) and I(return_content) is C(yes)
|
|
||||||
type: str
|
|
||||||
version_added: "2.10"
|
|
||||||
'''
|
|
||||||
|
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
|
|
||||||
MINIMAL_PYOPENSSL_VERSION = '16.0.0'
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH = '1.4'
|
|
||||||
|
|
||||||
PYOPENSSL_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import OpenSSL
|
|
||||||
from OpenSSL import crypto
|
|
||||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
|
||||||
except ImportError:
|
|
||||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
|
||||||
PYOPENSSL_FOUND = False
|
|
||||||
else:
|
|
||||||
PYOPENSSL_FOUND = True
|
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
|
||||||
from cryptography.hazmat.primitives import serialization as crypto_serialization
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
from ansible.module_utils import crypto as crypto_utils
|
|
||||||
from ansible.module_utils._text import to_native
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
|
|
||||||
|
|
||||||
class PublicKeyError(crypto_utils.OpenSSLObjectError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PublicKey(crypto_utils.OpenSSLObject):
|
|
||||||
|
|
||||||
def __init__(self, module, backend):
|
|
||||||
super(PublicKey, self).__init__(
|
|
||||||
module.params['path'],
|
|
||||||
module.params['state'],
|
|
||||||
module.params['force'],
|
|
||||||
module.check_mode
|
|
||||||
)
|
|
||||||
self.format = module.params['format']
|
|
||||||
self.privatekey_path = module.params['privatekey_path']
|
|
||||||
self.privatekey_content = module.params['privatekey_content']
|
|
||||||
if self.privatekey_content is not None:
|
|
||||||
self.privatekey_content = self.privatekey_content.encode('utf-8')
|
|
||||||
self.privatekey_passphrase = module.params['privatekey_passphrase']
|
|
||||||
self.privatekey = None
|
|
||||||
self.publickey_bytes = None
|
|
||||||
self.return_content = module.params['return_content']
|
|
||||||
self.fingerprint = {}
|
|
||||||
self.backend = backend
|
|
||||||
|
|
||||||
self.backup = module.params['backup']
|
|
||||||
self.backup_file = None
|
|
||||||
|
|
||||||
def _create_publickey(self, module):
|
|
||||||
self.privatekey = crypto_utils.load_privatekey(
|
|
||||||
path=self.privatekey_path,
|
|
||||||
content=self.privatekey_content,
|
|
||||||
passphrase=self.privatekey_passphrase,
|
|
||||||
backend=self.backend
|
|
||||||
)
|
|
||||||
if self.backend == 'cryptography':
|
|
||||||
if self.format == 'OpenSSH':
|
|
||||||
return self.privatekey.public_key().public_bytes(
|
|
||||||
crypto_serialization.Encoding.OpenSSH,
|
|
||||||
crypto_serialization.PublicFormat.OpenSSH
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return self.privatekey.public_key().public_bytes(
|
|
||||||
crypto_serialization.Encoding.PEM,
|
|
||||||
crypto_serialization.PublicFormat.SubjectPublicKeyInfo
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
return crypto.dump_publickey(crypto.FILETYPE_PEM, self.privatekey)
|
|
||||||
except AttributeError as dummy:
|
|
||||||
raise PublicKeyError('You need to have PyOpenSSL>=16.0.0 to generate public keys')
|
|
||||||
|
|
||||||
def generate(self, module):
|
|
||||||
"""Generate the public key."""
|
|
||||||
|
|
||||||
if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
|
|
||||||
raise PublicKeyError(
|
|
||||||
'The private key %s does not exist' % self.privatekey_path
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.check(module, perms_required=False) or self.force:
|
|
||||||
try:
|
|
||||||
publickey_content = self._create_publickey(module)
|
|
||||||
if self.return_content:
|
|
||||||
self.publickey_bytes = publickey_content
|
|
||||||
|
|
||||||
if self.backup:
|
|
||||||
self.backup_file = module.backup_local(self.path)
|
|
||||||
crypto_utils.write_file(module, publickey_content)
|
|
||||||
|
|
||||||
self.changed = True
|
|
||||||
except crypto_utils.OpenSSLBadPassphraseError as exc:
|
|
||||||
raise PublicKeyError(exc)
|
|
||||||
except (IOError, OSError) as exc:
|
|
||||||
raise PublicKeyError(exc)
|
|
||||||
|
|
||||||
self.fingerprint = crypto_utils.get_fingerprint(
|
|
||||||
path=self.privatekey_path,
|
|
||||||
content=self.privatekey_content,
|
|
||||||
passphrase=self.privatekey_passphrase,
|
|
||||||
backend=self.backend,
|
|
||||||
)
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
|
||||||
if module.set_fs_attributes_if_different(file_args, False):
|
|
||||||
self.changed = True
|
|
||||||
|
|
||||||
def check(self, module, perms_required=True):
|
|
||||||
"""Ensure the resource is in its desired state."""
|
|
||||||
|
|
||||||
state_and_perms = super(PublicKey, self).check(module, perms_required)
|
|
||||||
|
|
||||||
def _check_privatekey():
|
|
||||||
if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(self.path, 'rb') as public_key_fh:
|
|
||||||
publickey_content = public_key_fh.read()
|
|
||||||
if self.return_content:
|
|
||||||
self.publickey_bytes = publickey_content
|
|
||||||
if self.backend == 'cryptography':
|
|
||||||
if self.format == 'OpenSSH':
|
|
||||||
# Read and dump public key. Makes sure that the comment is stripped off.
|
|
||||||
current_publickey = crypto_serialization.load_ssh_public_key(publickey_content, backend=default_backend())
|
|
||||||
publickey_content = current_publickey.public_bytes(
|
|
||||||
crypto_serialization.Encoding.OpenSSH,
|
|
||||||
crypto_serialization.PublicFormat.OpenSSH
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
current_publickey = crypto_serialization.load_pem_public_key(publickey_content, backend=default_backend())
|
|
||||||
publickey_content = current_publickey.public_bytes(
|
|
||||||
crypto_serialization.Encoding.PEM,
|
|
||||||
crypto_serialization.PublicFormat.SubjectPublicKeyInfo
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
publickey_content = crypto.dump_publickey(
|
|
||||||
crypto.FILETYPE_PEM,
|
|
||||||
crypto.load_publickey(crypto.FILETYPE_PEM, publickey_content)
|
|
||||||
)
|
|
||||||
except Exception as dummy:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
desired_publickey = self._create_publickey(module)
|
|
||||||
except crypto_utils.OpenSSLBadPassphraseError as exc:
|
|
||||||
raise PublicKeyError(exc)
|
|
||||||
|
|
||||||
return publickey_content == desired_publickey
|
|
||||||
|
|
||||||
if not state_and_perms:
|
|
||||||
return state_and_perms
|
|
||||||
|
|
||||||
return _check_privatekey()
|
|
||||||
|
|
||||||
def remove(self, module):
|
|
||||||
if self.backup:
|
|
||||||
self.backup_file = module.backup_local(self.path)
|
|
||||||
super(PublicKey, self).remove(module)
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
"""Serialize the object into a dictionary."""
|
|
||||||
|
|
||||||
result = {
|
|
||||||
'privatekey': self.privatekey_path,
|
|
||||||
'filename': self.path,
|
|
||||||
'format': self.format,
|
|
||||||
'changed': self.changed,
|
|
||||||
'fingerprint': self.fingerprint,
|
|
||||||
}
|
|
||||||
if self.backup_file:
|
|
||||||
result['backup_file'] = self.backup_file
|
|
||||||
if self.return_content:
|
|
||||||
if self.publickey_bytes is None:
|
|
||||||
self.publickey_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True)
|
|
||||||
result['publickey'] = self.publickey_bytes.decode('utf-8') if self.publickey_bytes else None
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
|
||||||
force=dict(type='bool', default=False),
|
|
||||||
path=dict(type='path', required=True),
|
|
||||||
privatekey_path=dict(type='path'),
|
|
||||||
privatekey_content=dict(type='str'),
|
|
||||||
format=dict(type='str', default='PEM', choices=['OpenSSH', 'PEM']),
|
|
||||||
privatekey_passphrase=dict(type='str', no_log=True),
|
|
||||||
backup=dict(type='bool', default=False),
|
|
||||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
|
||||||
return_content=dict(type='bool', default=False),
|
|
||||||
),
|
|
||||||
supports_check_mode=True,
|
|
||||||
add_file_common_args=True,
|
|
||||||
required_if=[('state', 'present', ['privatekey_path', 'privatekey_content'], True)],
|
|
||||||
mutually_exclusive=(
|
|
||||||
['privatekey_path', 'privatekey_content'],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
minimal_cryptography_version = MINIMAL_CRYPTOGRAPHY_VERSION
|
|
||||||
if module.params['format'] == 'OpenSSH':
|
|
||||||
minimal_cryptography_version = MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH
|
|
||||||
|
|
||||||
backend = module.params['select_crypto_backend']
|
|
||||||
if backend == 'auto':
|
|
||||||
# Detection what is possible
|
|
||||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(minimal_cryptography_version)
|
|
||||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
|
||||||
|
|
||||||
# Decision
|
|
||||||
if can_use_cryptography:
|
|
||||||
backend = 'cryptography'
|
|
||||||
elif can_use_pyopenssl:
|
|
||||||
if module.params['format'] == 'OpenSSH':
|
|
||||||
module.fail_json(
|
|
||||||
msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR
|
|
||||||
)
|
|
||||||
backend = 'pyopenssl'
|
|
||||||
|
|
||||||
# Success?
|
|
||||||
if backend == 'auto':
|
|
||||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
|
||||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
|
||||||
minimal_cryptography_version,
|
|
||||||
MINIMAL_PYOPENSSL_VERSION))
|
|
||||||
|
|
||||||
if module.params['format'] == 'OpenSSH' and backend != 'cryptography':
|
|
||||||
module.fail_json(msg="Format OpenSSH requires the cryptography backend.")
|
|
||||||
|
|
||||||
if backend == 'pyopenssl':
|
|
||||||
if not PYOPENSSL_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
|
||||||
exception=PYOPENSSL_IMP_ERR)
|
|
||||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13')
|
|
||||||
elif backend == 'cryptography':
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(minimal_cryptography_version)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
|
|
||||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
|
||||||
if not os.path.isdir(base_dir):
|
|
||||||
module.fail_json(
|
|
||||||
name=base_dir,
|
|
||||||
msg="The directory '%s' does not exist or the file is not a directory" % base_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
public_key = PublicKey(module, backend)
|
|
||||||
|
|
||||||
if public_key.state == 'present':
|
|
||||||
if module.check_mode:
|
|
||||||
result = public_key.dump()
|
|
||||||
result['changed'] = module.params['force'] or not public_key.check(module)
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
public_key.generate(module)
|
|
||||||
else:
|
|
||||||
if module.check_mode:
|
|
||||||
result = public_key.dump()
|
|
||||||
result['changed'] = os.path.exists(module.params['path'])
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
public_key.remove(module)
|
|
||||||
|
|
||||||
result = public_key.dump()
|
|
||||||
module.exit_json(**result)
|
|
||||||
except crypto_utils.OpenSSLObjectError as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,783 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright: (c) 2019, Felix Fontein <felix@fontein.de>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
---
|
|
||||||
module: x509_crl
|
|
||||||
version_added: "2.10"
|
|
||||||
short_description: Generate Certificate Revocation Lists (CRLs)
|
|
||||||
description:
|
|
||||||
- This module allows one to (re)generate or update Certificate Revocation Lists (CRLs).
|
|
||||||
- Certificates on the revocation list can be either specified via serial number and (optionally) their issuer,
|
|
||||||
or as a path to a certificate file in PEM format.
|
|
||||||
requirements:
|
|
||||||
- cryptography >= 1.2
|
|
||||||
author:
|
|
||||||
- Felix Fontein (@felixfontein)
|
|
||||||
options:
|
|
||||||
state:
|
|
||||||
description:
|
|
||||||
- Whether the CRL file should exist or not, taking action if the state is different from what is stated.
|
|
||||||
type: str
|
|
||||||
default: present
|
|
||||||
choices: [ absent, present ]
|
|
||||||
|
|
||||||
mode:
|
|
||||||
description:
|
|
||||||
- Defines how to process entries of existing CRLs.
|
|
||||||
- If set to C(generate), makes sure that the CRL has the exact set of revoked certificates
|
|
||||||
as specified in I(revoked_certificates).
|
|
||||||
- If set to C(update), makes sure that the CRL contains the revoked certificates from
|
|
||||||
I(revoked_certificates), but can also contain other revoked certificates. If the CRL file
|
|
||||||
already exists, all entries from the existing CRL will also be included in the new CRL.
|
|
||||||
When using C(update), you might be interested in setting I(ignore_timestamps) to C(yes).
|
|
||||||
type: str
|
|
||||||
default: generate
|
|
||||||
choices: [ generate, update ]
|
|
||||||
|
|
||||||
force:
|
|
||||||
description:
|
|
||||||
- Should the CRL be forced to be regenerated.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
|
|
||||||
backup:
|
|
||||||
description:
|
|
||||||
- Create a backup file including a timestamp so you can get the original
|
|
||||||
CRL back if you overwrote it with a new one by accident.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- Remote absolute path where the generated CRL file should be created or is already located.
|
|
||||||
type: path
|
|
||||||
required: yes
|
|
||||||
|
|
||||||
privatekey_path:
|
|
||||||
description:
|
|
||||||
- Path to the CA's private key to use when signing the CRL.
|
|
||||||
- Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
|
|
||||||
type: path
|
|
||||||
|
|
||||||
privatekey_content:
|
|
||||||
description:
|
|
||||||
- The content of the CA's private key to use when signing the CRL.
|
|
||||||
- Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
|
|
||||||
type: str
|
|
||||||
|
|
||||||
privatekey_passphrase:
|
|
||||||
description:
|
|
||||||
- The passphrase for the I(privatekey_path).
|
|
||||||
- This is required if the private key is password protected.
|
|
||||||
type: str
|
|
||||||
|
|
||||||
issuer:
|
|
||||||
description:
|
|
||||||
- Key/value pairs that will be present in the issuer name field of the CRL.
|
|
||||||
- If you need to specify more than one value with the same key, use a list as value.
|
|
||||||
- Required if I(state) is C(present).
|
|
||||||
type: dict
|
|
||||||
|
|
||||||
last_update:
|
|
||||||
description:
|
|
||||||
- The point in time from which this CRL can be trusted.
|
|
||||||
- Time can be specified either as relative time or as absolute timestamp.
|
|
||||||
- Time will always be interpreted as UTC.
|
|
||||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
|
||||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
|
||||||
- Note that if using relative time this module is NOT idempotent, except when
|
|
||||||
I(ignore_timestamps) is set to C(yes).
|
|
||||||
type: str
|
|
||||||
default: "+0s"
|
|
||||||
|
|
||||||
next_update:
|
|
||||||
description:
|
|
||||||
- "The absolute latest point in time by which this I(issuer) is expected to have issued
|
|
||||||
another CRL. Many clients will treat a CRL as expired once I(next_update) occurs."
|
|
||||||
- Time can be specified either as relative time or as absolute timestamp.
|
|
||||||
- Time will always be interpreted as UTC.
|
|
||||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
|
||||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
|
||||||
- Note that if using relative time this module is NOT idempotent, except when
|
|
||||||
I(ignore_timestamps) is set to C(yes).
|
|
||||||
- Required if I(state) is C(present).
|
|
||||||
type: str
|
|
||||||
|
|
||||||
digest:
|
|
||||||
description:
|
|
||||||
- Digest algorithm to be used when signing the CRL.
|
|
||||||
type: str
|
|
||||||
default: sha256
|
|
||||||
|
|
||||||
revoked_certificates:
|
|
||||||
description:
|
|
||||||
- List of certificates to be revoked.
|
|
||||||
- Required if I(state) is C(present).
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
suboptions:
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- Path to a certificate in PEM format.
|
|
||||||
- The serial number and issuer will be extracted from the certificate.
|
|
||||||
- Mutually exclusive with I(content) and I(serial_number). One of these three options
|
|
||||||
must be specified.
|
|
||||||
type: path
|
|
||||||
content:
|
|
||||||
description:
|
|
||||||
- Content of a certificate in PEM format.
|
|
||||||
- The serial number and issuer will be extracted from the certificate.
|
|
||||||
- Mutually exclusive with I(path) and I(serial_number). One of these three options
|
|
||||||
must be specified.
|
|
||||||
type: str
|
|
||||||
serial_number:
|
|
||||||
description:
|
|
||||||
- Serial number of the certificate.
|
|
||||||
- Mutually exclusive with I(path) and I(content). One of these three options must
|
|
||||||
be specified.
|
|
||||||
type: int
|
|
||||||
revocation_date:
|
|
||||||
description:
|
|
||||||
- The point in time the certificate was revoked.
|
|
||||||
- Time can be specified either as relative time or as absolute timestamp.
|
|
||||||
- Time will always be interpreted as UTC.
|
|
||||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
|
||||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
|
||||||
- Note that if using relative time this module is NOT idempotent, except when
|
|
||||||
I(ignore_timestamps) is set to C(yes).
|
|
||||||
type: str
|
|
||||||
default: "+0s"
|
|
||||||
issuer:
|
|
||||||
description:
|
|
||||||
- The certificate's issuer.
|
|
||||||
- "Example: C(DNS:ca.example.org)"
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
issuer_critical:
|
|
||||||
description:
|
|
||||||
- Whether the certificate issuer extension should be critical.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
reason:
|
|
||||||
description:
|
|
||||||
- The value for the revocation reason extension.
|
|
||||||
type: str
|
|
||||||
choices:
|
|
||||||
- unspecified
|
|
||||||
- key_compromise
|
|
||||||
- ca_compromise
|
|
||||||
- affiliation_changed
|
|
||||||
- superseded
|
|
||||||
- cessation_of_operation
|
|
||||||
- certificate_hold
|
|
||||||
- privilege_withdrawn
|
|
||||||
- aa_compromise
|
|
||||||
- remove_from_crl
|
|
||||||
reason_critical:
|
|
||||||
description:
|
|
||||||
- Whether the revocation reason extension should be critical.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
invalidity_date:
|
|
||||||
description:
|
|
||||||
- The point in time it was known/suspected that the private key was compromised
|
|
||||||
or that the certificate otherwise became invalid.
|
|
||||||
- Time can be specified either as relative time or as absolute timestamp.
|
|
||||||
- Time will always be interpreted as UTC.
|
|
||||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
|
||||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
|
||||||
- Note that if using relative time this module is NOT idempotent. This will NOT
|
|
||||||
change when I(ignore_timestamps) is set to C(yes).
|
|
||||||
type: str
|
|
||||||
invalidity_date_critical:
|
|
||||||
description:
|
|
||||||
- Whether the invalidity date extension should be critical.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
|
|
||||||
ignore_timestamps:
|
|
||||||
description:
|
|
||||||
- Whether the timestamps I(last_update), I(next_update) and I(revocation_date) (in
|
|
||||||
I(revoked_certificates)) should be ignored for idempotency checks. The timestamp
|
|
||||||
I(invalidity_date) in I(revoked_certificates) will never be ignored.
|
|
||||||
- Use this in combination with relative timestamps for these values to get idempotency.
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
|
|
||||||
return_content:
|
|
||||||
description:
|
|
||||||
- If set to C(yes), will return the (current or generated) CRL's content as I(crl).
|
|
||||||
type: bool
|
|
||||||
default: no
|
|
||||||
|
|
||||||
extends_documentation_fragment:
|
|
||||||
- files
|
|
||||||
|
|
||||||
notes:
|
|
||||||
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
|
|
||||||
- Date specified should be UTC. Minutes and seconds are mandatory.
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Generate a CRL
|
|
||||||
x509_crl:
|
|
||||||
path: /etc/ssl/my-ca.crl
|
|
||||||
privatekey_path: /etc/ssl/private/my-ca.pem
|
|
||||||
issuer:
|
|
||||||
CN: My CA
|
|
||||||
last_update: "+0s"
|
|
||||||
next_update: "+7d"
|
|
||||||
revoked_certificates:
|
|
||||||
- serial_number: 1234
|
|
||||||
revocation_date: 20190331202428Z
|
|
||||||
issuer:
|
|
||||||
CN: My CA
|
|
||||||
- serial_number: 2345
|
|
||||||
revocation_date: 20191013152910Z
|
|
||||||
reason: affiliation_changed
|
|
||||||
invalidity_date: 20191001000000Z
|
|
||||||
- path: /etc/ssl/crt/revoked-cert.pem
|
|
||||||
revocation_date: 20191010010203Z
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = r'''
|
|
||||||
filename:
|
|
||||||
description: Path to the generated CRL
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: /path/to/my-ca.crl
|
|
||||||
backup_file:
|
|
||||||
description: Name of backup file created.
|
|
||||||
returned: changed and if I(backup) is C(yes)
|
|
||||||
type: str
|
|
||||||
sample: /path/to/my-ca.crl.2019-03-09@11:22~
|
|
||||||
privatekey:
|
|
||||||
description: Path to the private CA key
|
|
||||||
returned: changed or success
|
|
||||||
type: str
|
|
||||||
sample: /path/to/my-ca.pem
|
|
||||||
issuer:
|
|
||||||
description:
|
|
||||||
- The CRL's issuer.
|
|
||||||
- Note that for repeated values, only the last one will be returned.
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
|
|
||||||
issuer_ordered:
|
|
||||||
description: The CRL's issuer as an ordered list of tuples.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: list
|
|
||||||
sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]'
|
|
||||||
last_update:
|
|
||||||
description: The point in time from which this CRL can be trusted as ASN.1 TIME.
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: 20190413202428Z
|
|
||||||
next_update:
|
|
||||||
description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME.
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: 20190413202428Z
|
|
||||||
digest:
|
|
||||||
description: The signature algorithm used to sign the CRL.
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: sha256WithRSAEncryption
|
|
||||||
revoked_certificates:
|
|
||||||
description: List of certificates to be revoked.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
contains:
|
|
||||||
serial_number:
|
|
||||||
description: Serial number of the certificate.
|
|
||||||
type: int
|
|
||||||
sample: 1234
|
|
||||||
revocation_date:
|
|
||||||
description: The point in time the certificate was revoked as ASN.1 TIME.
|
|
||||||
type: str
|
|
||||||
sample: 20190413202428Z
|
|
||||||
issuer:
|
|
||||||
description: The certificate's issuer.
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
sample: '["DNS:ca.example.org"]'
|
|
||||||
issuer_critical:
|
|
||||||
description: Whether the certificate issuer extension is critical.
|
|
||||||
type: bool
|
|
||||||
sample: no
|
|
||||||
reason:
|
|
||||||
description:
|
|
||||||
- The value for the revocation reason extension.
|
|
||||||
- One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded),
|
|
||||||
C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and
|
|
||||||
C(remove_from_crl).
|
|
||||||
type: str
|
|
||||||
sample: key_compromise
|
|
||||||
reason_critical:
|
|
||||||
description: Whether the revocation reason extension is critical.
|
|
||||||
type: bool
|
|
||||||
sample: no
|
|
||||||
invalidity_date:
|
|
||||||
description: |
|
|
||||||
The point in time it was known/suspected that the private key was compromised
|
|
||||||
or that the certificate otherwise became invalid as ASN.1 TIME.
|
|
||||||
type: str
|
|
||||||
sample: 20190413202428Z
|
|
||||||
invalidity_date_critical:
|
|
||||||
description: Whether the invalidity date extension is critical.
|
|
||||||
type: bool
|
|
||||||
sample: no
|
|
||||||
crl:
|
|
||||||
description: The (current or generated) CRL's content.
|
|
||||||
returned: if I(state) is C(present) and I(return_content) is C(yes)
|
|
||||||
type: str
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
|
|
||||||
from ansible.module_utils import crypto as crypto_utils
|
|
||||||
from ansible.module_utils._text import to_native, to_text
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
|
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
from cryptography import x509
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
|
||||||
from cryptography.hazmat.primitives.serialization import Encoding
|
|
||||||
from cryptography.x509 import (
|
|
||||||
CertificateRevocationListBuilder,
|
|
||||||
RevokedCertificateBuilder,
|
|
||||||
NameAttribute,
|
|
||||||
Name,
|
|
||||||
)
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
|
|
||||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
|
||||||
|
|
||||||
|
|
||||||
class CRLError(crypto_utils.OpenSSLObjectError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CRL(crypto_utils.OpenSSLObject):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(CRL, self).__init__(
|
|
||||||
module.params['path'],
|
|
||||||
module.params['state'],
|
|
||||||
module.params['force'],
|
|
||||||
module.check_mode
|
|
||||||
)
|
|
||||||
|
|
||||||
self.update = module.params['mode'] == 'update'
|
|
||||||
self.ignore_timestamps = module.params['ignore_timestamps']
|
|
||||||
self.return_content = module.params['return_content']
|
|
||||||
self.crl_content = None
|
|
||||||
|
|
||||||
self.privatekey_path = module.params['privatekey_path']
|
|
||||||
self.privatekey_content = module.params['privatekey_content']
|
|
||||||
if self.privatekey_content is not None:
|
|
||||||
self.privatekey_content = self.privatekey_content.encode('utf-8')
|
|
||||||
self.privatekey_passphrase = module.params['privatekey_passphrase']
|
|
||||||
|
|
||||||
self.issuer = crypto_utils.parse_name_field(module.params['issuer'])
|
|
||||||
self.issuer = [(entry[0], entry[1]) for entry in self.issuer if entry[1]]
|
|
||||||
|
|
||||||
self.last_update = crypto_utils.get_relative_time_option(module.params['last_update'], 'last_update')
|
|
||||||
self.next_update = crypto_utils.get_relative_time_option(module.params['next_update'], 'next_update')
|
|
||||||
|
|
||||||
self.digest = crypto_utils.select_message_digest(module.params['digest'])
|
|
||||||
if self.digest is None:
|
|
||||||
raise CRLError('The digest "{0}" is not supported'.format(module.params['digest']))
|
|
||||||
|
|
||||||
self.revoked_certificates = []
|
|
||||||
for i, rc in enumerate(module.params['revoked_certificates']):
|
|
||||||
result = {
|
|
||||||
'serial_number': None,
|
|
||||||
'revocation_date': None,
|
|
||||||
'issuer': None,
|
|
||||||
'issuer_critical': False,
|
|
||||||
'reason': None,
|
|
||||||
'reason_critical': False,
|
|
||||||
'invalidity_date': None,
|
|
||||||
'invalidity_date_critical': False,
|
|
||||||
}
|
|
||||||
path_prefix = 'revoked_certificates[{0}].'.format(i)
|
|
||||||
if rc['path'] is not None or rc['content'] is not None:
|
|
||||||
# Load certificate from file or content
|
|
||||||
try:
|
|
||||||
if rc['content'] is not None:
|
|
||||||
rc['content'] = rc['content'].encode('utf-8')
|
|
||||||
cert = crypto_utils.load_certificate(rc['path'], content=rc['content'], backend='cryptography')
|
|
||||||
try:
|
|
||||||
result['serial_number'] = cert.serial_number
|
|
||||||
except AttributeError:
|
|
||||||
# The property was called "serial" before cryptography 1.4
|
|
||||||
result['serial_number'] = cert.serial
|
|
||||||
except crypto_utils.OpenSSLObjectError as e:
|
|
||||||
if rc['content'] is not None:
|
|
||||||
module.fail_json(
|
|
||||||
msg='Cannot parse certificate from {0}content: {1}'.format(path_prefix, to_native(e))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
module.fail_json(
|
|
||||||
msg='Cannot read certificate "{1}" from {0}path: {2}'.format(path_prefix, rc['path'], to_native(e))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Specify serial_number (and potentially issuer) directly
|
|
||||||
result['serial_number'] = rc['serial_number']
|
|
||||||
# All other options
|
|
||||||
if rc['issuer']:
|
|
||||||
result['issuer'] = [crypto_utils.cryptography_get_name(issuer) for issuer in rc['issuer']]
|
|
||||||
result['issuer_critical'] = rc['issuer_critical']
|
|
||||||
result['revocation_date'] = crypto_utils.get_relative_time_option(
|
|
||||||
rc['revocation_date'],
|
|
||||||
path_prefix + 'revocation_date'
|
|
||||||
)
|
|
||||||
if rc['reason']:
|
|
||||||
result['reason'] = crypto_utils.REVOCATION_REASON_MAP[rc['reason']]
|
|
||||||
result['reason_critical'] = rc['reason_critical']
|
|
||||||
if rc['invalidity_date']:
|
|
||||||
result['invalidity_date'] = crypto_utils.get_relative_time_option(
|
|
||||||
rc['invalidity_date'],
|
|
||||||
path_prefix + 'invalidity_date'
|
|
||||||
)
|
|
||||||
result['invalidity_date_critical'] = rc['invalidity_date_critical']
|
|
||||||
self.revoked_certificates.append(result)
|
|
||||||
|
|
||||||
self.module = module
|
|
||||||
|
|
||||||
self.backup = module.params['backup']
|
|
||||||
self.backup_file = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.privatekey = crypto_utils.load_privatekey(
|
|
||||||
path=self.privatekey_path,
|
|
||||||
content=self.privatekey_content,
|
|
||||||
passphrase=self.privatekey_passphrase,
|
|
||||||
backend='cryptography'
|
|
||||||
)
|
|
||||||
except crypto_utils.OpenSSLBadPassphraseError as exc:
|
|
||||||
raise CRLError(exc)
|
|
||||||
|
|
||||||
self.crl = None
|
|
||||||
try:
|
|
||||||
with open(self.path, 'rb') as f:
|
|
||||||
data = f.read()
|
|
||||||
self.crl = x509.load_pem_x509_crl(data, default_backend())
|
|
||||||
if self.return_content:
|
|
||||||
self.crl_content = data
|
|
||||||
except Exception as dummy:
|
|
||||||
self.crl_content = None
|
|
||||||
|
|
||||||
def remove(self):
|
|
||||||
if self.backup:
|
|
||||||
self.backup_file = self.module.backup_local(self.path)
|
|
||||||
super(CRL, self).remove(self.module)
|
|
||||||
|
|
||||||
def _compress_entry(self, entry):
|
|
||||||
if self.ignore_timestamps:
|
|
||||||
# Throw out revocation_date
|
|
||||||
return (
|
|
||||||
entry['serial_number'],
|
|
||||||
tuple(entry['issuer']) if entry['issuer'] is not None else None,
|
|
||||||
entry['issuer_critical'],
|
|
||||||
entry['reason'],
|
|
||||||
entry['reason_critical'],
|
|
||||||
entry['invalidity_date'],
|
|
||||||
entry['invalidity_date_critical'],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return (
|
|
||||||
entry['serial_number'],
|
|
||||||
entry['revocation_date'],
|
|
||||||
tuple(entry['issuer']) if entry['issuer'] is not None else None,
|
|
||||||
entry['issuer_critical'],
|
|
||||||
entry['reason'],
|
|
||||||
entry['reason_critical'],
|
|
||||||
entry['invalidity_date'],
|
|
||||||
entry['invalidity_date_critical'],
|
|
||||||
)
|
|
||||||
|
|
||||||
def check(self, perms_required=True):
|
|
||||||
"""Ensure the resource is in its desired state."""
|
|
||||||
|
|
||||||
state_and_perms = super(CRL, self).check(self.module, perms_required)
|
|
||||||
|
|
||||||
if not state_and_perms:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.crl is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.last_update != self.crl.last_update and not self.ignore_timestamps:
|
|
||||||
return False
|
|
||||||
if self.next_update != self.crl.next_update and not self.ignore_timestamps:
|
|
||||||
return False
|
|
||||||
if self.digest.name != self.crl.signature_hash_algorithm.name:
|
|
||||||
return False
|
|
||||||
|
|
||||||
want_issuer = [(crypto_utils.cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer]
|
|
||||||
if want_issuer != [(sub.oid, sub.value) for sub in self.crl.issuer]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
old_entries = [self._compress_entry(crypto_utils.cryptography_decode_revoked_certificate(cert)) for cert in self.crl]
|
|
||||||
new_entries = [self._compress_entry(cert) for cert in self.revoked_certificates]
|
|
||||||
if self.update:
|
|
||||||
# We don't simply use a set so that duplicate entries are treated correctly
|
|
||||||
for entry in new_entries:
|
|
||||||
try:
|
|
||||||
old_entries.remove(entry)
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
if old_entries != new_entries:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _generate_crl(self):
|
|
||||||
backend = default_backend()
|
|
||||||
crl = CertificateRevocationListBuilder()
|
|
||||||
|
|
||||||
try:
|
|
||||||
crl = crl.issuer_name(Name([
|
|
||||||
NameAttribute(crypto_utils.cryptography_name_to_oid(entry[0]), to_text(entry[1]))
|
|
||||||
for entry in self.issuer
|
|
||||||
]))
|
|
||||||
except ValueError as e:
|
|
||||||
raise CRLError(e)
|
|
||||||
|
|
||||||
crl = crl.last_update(self.last_update)
|
|
||||||
crl = crl.next_update(self.next_update)
|
|
||||||
|
|
||||||
if self.update and self.crl:
|
|
||||||
new_entries = set([self._compress_entry(entry) for entry in self.revoked_certificates])
|
|
||||||
for entry in self.crl:
|
|
||||||
decoded_entry = self._compress_entry(crypto_utils.cryptography_decode_revoked_certificate(entry))
|
|
||||||
if decoded_entry not in new_entries:
|
|
||||||
crl = crl.add_revoked_certificate(entry)
|
|
||||||
for entry in self.revoked_certificates:
|
|
||||||
revoked_cert = RevokedCertificateBuilder()
|
|
||||||
revoked_cert = revoked_cert.serial_number(entry['serial_number'])
|
|
||||||
revoked_cert = revoked_cert.revocation_date(entry['revocation_date'])
|
|
||||||
if entry['issuer'] is not None:
|
|
||||||
revoked_cert = revoked_cert.add_extension(
|
|
||||||
x509.CertificateIssuer([
|
|
||||||
crypto_utils.cryptography_get_name(name) for name in self.entry['issuer']
|
|
||||||
]),
|
|
||||||
entry['issuer_critical']
|
|
||||||
)
|
|
||||||
if entry['reason'] is not None:
|
|
||||||
revoked_cert = revoked_cert.add_extension(
|
|
||||||
x509.CRLReason(entry['reason']),
|
|
||||||
entry['reason_critical']
|
|
||||||
)
|
|
||||||
if entry['invalidity_date'] is not None:
|
|
||||||
revoked_cert = revoked_cert.add_extension(
|
|
||||||
x509.InvalidityDate(entry['invalidity_date']),
|
|
||||||
entry['invalidity_date_critical']
|
|
||||||
)
|
|
||||||
crl = crl.add_revoked_certificate(revoked_cert.build(backend))
|
|
||||||
|
|
||||||
self.crl = crl.sign(self.privatekey, self.digest, backend=backend)
|
|
||||||
return self.crl.public_bytes(Encoding.PEM)
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
if not self.check(perms_required=False) or self.force:
|
|
||||||
result = self._generate_crl()
|
|
||||||
if self.return_content:
|
|
||||||
self.crl_content = result
|
|
||||||
if self.backup:
|
|
||||||
self.backup_file = self.module.backup_local(self.path)
|
|
||||||
crypto_utils.write_file(self.module, result)
|
|
||||||
self.changed = True
|
|
||||||
|
|
||||||
file_args = self.module.load_file_common_arguments(self.module.params)
|
|
||||||
if self.module.set_fs_attributes_if_different(file_args, False):
|
|
||||||
self.changed = True
|
|
||||||
|
|
||||||
def _dump_revoked(self, entry):
|
|
||||||
return {
|
|
||||||
'serial_number': entry['serial_number'],
|
|
||||||
'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT),
|
|
||||||
'issuer':
|
|
||||||
[crypto_utils.cryptography_decode_name(issuer) for issuer in entry['issuer']]
|
|
||||||
if entry['issuer'] is not None else None,
|
|
||||||
'issuer_critical': entry['issuer_critical'],
|
|
||||||
'reason': crypto_utils.REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None,
|
|
||||||
'reason_critical': entry['reason_critical'],
|
|
||||||
'invalidity_date':
|
|
||||||
entry['invalidity_date'].strftime(TIMESTAMP_FORMAT)
|
|
||||||
if entry['invalidity_date'] is not None else None,
|
|
||||||
'invalidity_date_critical': entry['invalidity_date_critical'],
|
|
||||||
}
|
|
||||||
|
|
||||||
def dump(self, check_mode=False):
|
|
||||||
result = {
|
|
||||||
'changed': self.changed,
|
|
||||||
'filename': self.path,
|
|
||||||
'privatekey': self.privatekey_path,
|
|
||||||
'last_update': None,
|
|
||||||
'next_update': None,
|
|
||||||
'digest': None,
|
|
||||||
'issuer_ordered': None,
|
|
||||||
'issuer': None,
|
|
||||||
'revoked_certificates': [],
|
|
||||||
}
|
|
||||||
if self.backup_file:
|
|
||||||
result['backup_file'] = self.backup_file
|
|
||||||
|
|
||||||
if check_mode:
|
|
||||||
result['last_update'] = self.last_update.strftime(TIMESTAMP_FORMAT)
|
|
||||||
result['next_update'] = self.next_update.strftime(TIMESTAMP_FORMAT)
|
|
||||||
# result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid)
|
|
||||||
result['digest'] = self.module.params['digest']
|
|
||||||
result['issuer_ordered'] = self.issuer
|
|
||||||
result['issuer'] = {}
|
|
||||||
for k, v in self.issuer:
|
|
||||||
result['issuer'][k] = v
|
|
||||||
result['revoked_certificates'] = []
|
|
||||||
for entry in self.revoked_certificates:
|
|
||||||
result['revoked_certificates'].append(self._dump_revoked(entry))
|
|
||||||
elif self.crl:
|
|
||||||
result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
|
|
||||||
result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
|
|
||||||
try:
|
|
||||||
result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid)
|
|
||||||
except AttributeError:
|
|
||||||
# Older cryptography versions don't have signature_algorithm_oid yet
|
|
||||||
dotted = crypto_utils._obj2txt(
|
|
||||||
self.crl._backend._lib,
|
|
||||||
self.crl._backend._ffi,
|
|
||||||
self.crl._x509_crl.sig_alg.algorithm
|
|
||||||
)
|
|
||||||
oid = x509.oid.ObjectIdentifier(dotted)
|
|
||||||
result['digest'] = crypto_utils.cryptography_oid_to_name(oid)
|
|
||||||
issuer = []
|
|
||||||
for attribute in self.crl.issuer:
|
|
||||||
issuer.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value])
|
|
||||||
result['issuer_ordered'] = issuer
|
|
||||||
result['issuer'] = {}
|
|
||||||
for k, v in issuer:
|
|
||||||
result['issuer'][k] = v
|
|
||||||
result['revoked_certificates'] = []
|
|
||||||
for cert in self.crl:
|
|
||||||
entry = crypto_utils.cryptography_decode_revoked_certificate(cert)
|
|
||||||
result['revoked_certificates'].append(self._dump_revoked(entry))
|
|
||||||
|
|
||||||
if self.return_content:
|
|
||||||
result['crl'] = self.crl_content
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
|
||||||
mode=dict(type='str', default='generate', choices=['generate', 'update']),
|
|
||||||
force=dict(type='bool', default=False),
|
|
||||||
backup=dict(type='bool', default=False),
|
|
||||||
path=dict(type='path', required=True),
|
|
||||||
privatekey_path=dict(type='path'),
|
|
||||||
privatekey_content=dict(type='str'),
|
|
||||||
privatekey_passphrase=dict(type='str', no_log=True),
|
|
||||||
issuer=dict(type='dict'),
|
|
||||||
last_update=dict(type='str', default='+0s'),
|
|
||||||
next_update=dict(type='str'),
|
|
||||||
digest=dict(type='str', default='sha256'),
|
|
||||||
ignore_timestamps=dict(type='bool', default=False),
|
|
||||||
return_content=dict(type='bool', default=False),
|
|
||||||
revoked_certificates=dict(
|
|
||||||
type='list',
|
|
||||||
elements='dict',
|
|
||||||
options=dict(
|
|
||||||
path=dict(type='path'),
|
|
||||||
content=dict(type='str'),
|
|
||||||
serial_number=dict(type='int'),
|
|
||||||
revocation_date=dict(type='str', default='+0s'),
|
|
||||||
issuer=dict(type='list', elements='str'),
|
|
||||||
issuer_critical=dict(type='bool', default=False),
|
|
||||||
reason=dict(
|
|
||||||
type='str',
|
|
||||||
choices=[
|
|
||||||
'unspecified', 'key_compromise', 'ca_compromise', 'affiliation_changed',
|
|
||||||
'superseded', 'cessation_of_operation', 'certificate_hold',
|
|
||||||
'privilege_withdrawn', 'aa_compromise', 'remove_from_crl'
|
|
||||||
]
|
|
||||||
),
|
|
||||||
reason_critical=dict(type='bool', default=False),
|
|
||||||
invalidity_date=dict(type='str'),
|
|
||||||
invalidity_date_critical=dict(type='bool', default=False),
|
|
||||||
),
|
|
||||||
required_one_of=[['path', 'content', 'serial_number']],
|
|
||||||
mutually_exclusive=[['path', 'content', 'serial_number']],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
required_if=[
|
|
||||||
('state', 'present', ['privatekey_path', 'privatekey_content'], True),
|
|
||||||
('state', 'present', ['issuer', 'next_update', 'revoked_certificates'], False),
|
|
||||||
],
|
|
||||||
mutually_exclusive=(
|
|
||||||
['privatekey_path', 'privatekey_content'],
|
|
||||||
),
|
|
||||||
supports_check_mode=True,
|
|
||||||
add_file_common_args=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
|
|
||||||
try:
|
|
||||||
crl = CRL(module)
|
|
||||||
|
|
||||||
if module.params['state'] == 'present':
|
|
||||||
if module.check_mode:
|
|
||||||
result = crl.dump(check_mode=True)
|
|
||||||
result['changed'] = module.params['force'] or not crl.check()
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
crl.generate()
|
|
||||||
else:
|
|
||||||
if module.check_mode:
|
|
||||||
result = crl.dump(check_mode=True)
|
|
||||||
result['changed'] = os.path.exists(module.params['path'])
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
crl.remove()
|
|
||||||
|
|
||||||
result = crl.dump()
|
|
||||||
module.exit_json(**result)
|
|
||||||
except crypto_utils.OpenSSLObjectError as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
@ -1,281 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
||||||
'status': ['preview'],
|
|
||||||
'supported_by': 'community'}
|
|
||||||
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
---
|
|
||||||
module: x509_crl_info
|
|
||||||
version_added: "2.10"
|
|
||||||
short_description: Retrieve information on Certificate Revocation Lists (CRLs)
|
|
||||||
description:
|
|
||||||
- This module allows one to retrieve information on Certificate Revocation Lists (CRLs).
|
|
||||||
requirements:
|
|
||||||
- cryptography >= 1.2
|
|
||||||
author:
|
|
||||||
- Felix Fontein (@felixfontein)
|
|
||||||
options:
|
|
||||||
path:
|
|
||||||
description:
|
|
||||||
- Remote absolute path where the generated CRL file should be created or is already located.
|
|
||||||
- Either I(path) or I(content) must be specified, but not both.
|
|
||||||
type: path
|
|
||||||
content:
|
|
||||||
description:
|
|
||||||
- Content of the X.509 certificate in PEM format.
|
|
||||||
- Either I(path) or I(content) must be specified, but not both.
|
|
||||||
type: str
|
|
||||||
|
|
||||||
notes:
|
|
||||||
- All timestamp values are provided in ASN.1 TIME format, i.e. following the C(YYYYMMDDHHMMSSZ) pattern.
|
|
||||||
They are all in UTC.
|
|
||||||
seealso:
|
|
||||||
- module: x509_crl
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLES = r'''
|
|
||||||
- name: Get information on CRL
|
|
||||||
x509_crl_info:
|
|
||||||
path: /etc/ssl/my-ca.crl
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- debug:
|
|
||||||
msg: "{{ result }}"
|
|
||||||
'''
|
|
||||||
|
|
||||||
RETURN = r'''
|
|
||||||
issuer:
|
|
||||||
description:
|
|
||||||
- The CRL's issuer.
|
|
||||||
- Note that for repeated values, only the last one will be returned.
|
|
||||||
returned: success
|
|
||||||
type: dict
|
|
||||||
sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
|
|
||||||
issuer_ordered:
|
|
||||||
description: The CRL's issuer as an ordered list of tuples.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: list
|
|
||||||
sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]'
|
|
||||||
last_update:
|
|
||||||
description: The point in time from which this CRL can be trusted as ASN.1 TIME.
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: 20190413202428Z
|
|
||||||
next_update:
|
|
||||||
description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME.
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: 20190413202428Z
|
|
||||||
digest:
|
|
||||||
description: The signature algorithm used to sign the CRL.
|
|
||||||
returned: success
|
|
||||||
type: str
|
|
||||||
sample: sha256WithRSAEncryption
|
|
||||||
revoked_certificates:
|
|
||||||
description: List of certificates to be revoked.
|
|
||||||
returned: success
|
|
||||||
type: list
|
|
||||||
elements: dict
|
|
||||||
contains:
|
|
||||||
serial_number:
|
|
||||||
description: Serial number of the certificate.
|
|
||||||
type: int
|
|
||||||
sample: 1234
|
|
||||||
revocation_date:
|
|
||||||
description: The point in time the certificate was revoked as ASN.1 TIME.
|
|
||||||
type: str
|
|
||||||
sample: 20190413202428Z
|
|
||||||
issuer:
|
|
||||||
description: The certificate's issuer.
|
|
||||||
type: list
|
|
||||||
elements: str
|
|
||||||
sample: '["DNS:ca.example.org"]'
|
|
||||||
issuer_critical:
|
|
||||||
description: Whether the certificate issuer extension is critical.
|
|
||||||
type: bool
|
|
||||||
sample: no
|
|
||||||
reason:
|
|
||||||
description:
|
|
||||||
- The value for the revocation reason extension.
|
|
||||||
- One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded),
|
|
||||||
C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and
|
|
||||||
C(remove_from_crl).
|
|
||||||
type: str
|
|
||||||
sample: key_compromise
|
|
||||||
reason_critical:
|
|
||||||
description: Whether the revocation reason extension is critical.
|
|
||||||
type: bool
|
|
||||||
sample: no
|
|
||||||
invalidity_date:
|
|
||||||
description: |
|
|
||||||
The point in time it was known/suspected that the private key was compromised
|
|
||||||
or that the certificate otherwise became invalid as ASN.1 TIME.
|
|
||||||
type: str
|
|
||||||
sample: 20190413202428Z
|
|
||||||
invalidity_date_critical:
|
|
||||||
description: Whether the invalidity date extension is critical.
|
|
||||||
type: bool
|
|
||||||
sample: no
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
import traceback
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
|
|
||||||
from ansible.module_utils import crypto as crypto_utils
|
|
||||||
from ansible.module_utils._text import to_native
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
|
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
from cryptography import x509
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
|
|
||||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
|
||||||
|
|
||||||
|
|
||||||
class CRLError(crypto_utils.OpenSSLObjectError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CRLInfo(crypto_utils.OpenSSLObject):
|
|
||||||
"""The main module implementation."""
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(CRLInfo, self).__init__(
|
|
||||||
module.params['path'] or '',
|
|
||||||
'present',
|
|
||||||
False,
|
|
||||||
module.check_mode
|
|
||||||
)
|
|
||||||
|
|
||||||
self.content = module.params['content']
|
|
||||||
|
|
||||||
self.module = module
|
|
||||||
|
|
||||||
self.crl = None
|
|
||||||
if self.content is None:
|
|
||||||
try:
|
|
||||||
with open(self.path, 'rb') as f:
|
|
||||||
data = f.read()
|
|
||||||
except Exception as e:
|
|
||||||
self.module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e))
|
|
||||||
else:
|
|
||||||
data = self.content.encode('utf-8')
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.crl = x509.load_pem_x509_crl(data, default_backend())
|
|
||||||
except Exception as e:
|
|
||||||
self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e))
|
|
||||||
|
|
||||||
def _dump_revoked(self, entry):
|
|
||||||
return {
|
|
||||||
'serial_number': entry['serial_number'],
|
|
||||||
'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT),
|
|
||||||
'issuer':
|
|
||||||
[crypto_utils.cryptography_decode_name(issuer) for issuer in entry['issuer']]
|
|
||||||
if entry['issuer'] is not None else None,
|
|
||||||
'issuer_critical': entry['issuer_critical'],
|
|
||||||
'reason': crypto_utils.REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None,
|
|
||||||
'reason_critical': entry['reason_critical'],
|
|
||||||
'invalidity_date':
|
|
||||||
entry['invalidity_date'].strftime(TIMESTAMP_FORMAT)
|
|
||||||
if entry['invalidity_date'] is not None else None,
|
|
||||||
'invalidity_date_critical': entry['invalidity_date_critical'],
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_info(self):
|
|
||||||
result = {
|
|
||||||
'changed': False,
|
|
||||||
'last_update': None,
|
|
||||||
'next_update': None,
|
|
||||||
'digest': None,
|
|
||||||
'issuer_ordered': None,
|
|
||||||
'issuer': None,
|
|
||||||
'revoked_certificates': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
|
|
||||||
result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
|
|
||||||
try:
|
|
||||||
result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid)
|
|
||||||
except AttributeError:
|
|
||||||
# Older cryptography versions don't have signature_algorithm_oid yet
|
|
||||||
dotted = crypto_utils._obj2txt(
|
|
||||||
self.crl._backend._lib,
|
|
||||||
self.crl._backend._ffi,
|
|
||||||
self.crl._x509_crl.sig_alg.algorithm
|
|
||||||
)
|
|
||||||
oid = x509.oid.ObjectIdentifier(dotted)
|
|
||||||
result['digest'] = crypto_utils.cryptography_oid_to_name(oid)
|
|
||||||
issuer = []
|
|
||||||
for attribute in self.crl.issuer:
|
|
||||||
issuer.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value])
|
|
||||||
result['issuer_ordered'] = issuer
|
|
||||||
result['issuer'] = {}
|
|
||||||
for k, v in issuer:
|
|
||||||
result['issuer'][k] = v
|
|
||||||
result['revoked_certificates'] = []
|
|
||||||
for cert in self.crl:
|
|
||||||
entry = crypto_utils.cryptography_decode_revoked_certificate(cert)
|
|
||||||
result['revoked_certificates'].append(self._dump_revoked(entry))
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
# Empty method because crypto_utils.OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
# Empty method because crypto_utils.OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=dict(
|
|
||||||
path=dict(type='path'),
|
|
||||||
content=dict(type='str'),
|
|
||||||
),
|
|
||||||
required_one_of=(
|
|
||||||
['path', 'content'],
|
|
||||||
),
|
|
||||||
mutually_exclusive=(
|
|
||||||
['path', 'content'],
|
|
||||||
),
|
|
||||||
supports_check_mode=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
|
|
||||||
try:
|
|
||||||
crl = CRLInfo(module)
|
|
||||||
result = crl.get_info()
|
|
||||||
module.exit_json(**result)
|
|
||||||
except crypto_utils.OpenSSLObjectError as e:
|
|
||||||
module.fail_json(msg=to_native(e))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
@ -1,118 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleDocFragment(object):
|
|
||||||
|
|
||||||
# Standard files documentation fragment
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
notes:
|
|
||||||
- "If a new enough version of the C(cryptography) library
|
|
||||||
is available (see Requirements for details), it will be used
|
|
||||||
instead of the C(openssl) binary. This can be explicitly disabled
|
|
||||||
or enabled with the C(select_crypto_backend) option. Note that using
|
|
||||||
the C(openssl) binary will be slower and less secure, as private key
|
|
||||||
contents always have to be stored on disk (see
|
|
||||||
C(account_key_content))."
|
|
||||||
- "Although the defaults are chosen so that the module can be used with
|
|
||||||
the L(Let's Encrypt,https://letsencrypt.org/) CA, the module can in
|
|
||||||
principle be used with any CA providing an ACME endpoint, such as
|
|
||||||
L(Buypass Go SSL,https://www.buypass.com/ssl/products/acme)."
|
|
||||||
requirements:
|
|
||||||
- python >= 2.6
|
|
||||||
- either openssl or L(cryptography,https://cryptography.io/) >= 1.5
|
|
||||||
options:
|
|
||||||
account_key_src:
|
|
||||||
description:
|
|
||||||
- "Path to a file containing the ACME account RSA or Elliptic Curve
|
|
||||||
key."
|
|
||||||
- "RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys can
|
|
||||||
be created with C(openssl ecparam -genkey ...). Any other tool creating
|
|
||||||
private keys in PEM format can be used as well."
|
|
||||||
- "Mutually exclusive with C(account_key_content)."
|
|
||||||
- "Required if C(account_key_content) is not used."
|
|
||||||
type: path
|
|
||||||
aliases: [ account_key ]
|
|
||||||
account_key_content:
|
|
||||||
description:
|
|
||||||
- "Content of the ACME account RSA or Elliptic Curve key."
|
|
||||||
- "Mutually exclusive with C(account_key_src)."
|
|
||||||
- "Required if C(account_key_src) is not used."
|
|
||||||
- "*Warning:* the content will be written into a temporary file, which will
|
|
||||||
be deleted by Ansible when the module completes. Since this is an
|
|
||||||
important private key — it can be used to change the account key,
|
|
||||||
or to revoke your certificates without knowing their private keys
|
|
||||||
—, this might not be acceptable."
|
|
||||||
- "In case C(cryptography) is used, the content is not written into a
|
|
||||||
temporary file. It can still happen that it is written to disk by
|
|
||||||
Ansible in the process of moving the module with its argument to
|
|
||||||
the node where it is executed."
|
|
||||||
type: str
|
|
||||||
version_added: "2.5"
|
|
||||||
account_uri:
|
|
||||||
description:
|
|
||||||
- "If specified, assumes that the account URI is as given. If the
|
|
||||||
account key does not match this account, or an account with this
|
|
||||||
URI does not exist, the module fails."
|
|
||||||
type: str
|
|
||||||
version_added: "2.7"
|
|
||||||
acme_version:
|
|
||||||
description:
|
|
||||||
- "The ACME version of the endpoint."
|
|
||||||
- "Must be 1 for the classic Let's Encrypt and Buypass ACME endpoints,
|
|
||||||
or 2 for standardized ACME v2 endpoints."
|
|
||||||
- "The default value is 1. Note that in Ansible 2.14, this option *will
|
|
||||||
be required* and will no longer have a default."
|
|
||||||
- "Please also note that we will deprecate ACME v1 support eventually."
|
|
||||||
type: int
|
|
||||||
choices: [ 1, 2 ]
|
|
||||||
version_added: "2.5"
|
|
||||||
acme_directory:
|
|
||||||
description:
|
|
||||||
- "The ACME directory to use. This is the entry point URL to access
|
|
||||||
CA server API."
|
|
||||||
- "For safety reasons the default is set to the Let's Encrypt staging
|
|
||||||
server (for the ACME v1 protocol). This will create technically correct,
|
|
||||||
but untrusted certificates."
|
|
||||||
- "The default value is U(https://acme-staging.api.letsencrypt.org/directory).
|
|
||||||
Note that in Ansible 2.14, this option *will be required* and will no longer
|
|
||||||
have a default."
|
|
||||||
- "For Let's Encrypt, all staging endpoints can be found here:
|
|
||||||
U(https://letsencrypt.org/docs/staging-environment/). For Buypass, all
|
|
||||||
endpoints can be found here:
|
|
||||||
U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)"
|
|
||||||
- "For Let's Encrypt, the production directory URL for ACME v1 is
|
|
||||||
U(https://acme-v01.api.letsencrypt.org/directory), and the production
|
|
||||||
directory URL for ACME v2 is U(https://acme-v02.api.letsencrypt.org/directory)."
|
|
||||||
- "For Buypass, the production directory URL for ACME v2 and v1 is
|
|
||||||
U(https://api.buypass.com/acme/directory)."
|
|
||||||
- "*Warning:* So far, the module has only been tested against Let's Encrypt
|
|
||||||
(staging and production), Buypass (staging and production), and
|
|
||||||
L(Pebble testing server,https://github.com/letsencrypt/Pebble)."
|
|
||||||
type: str
|
|
||||||
validate_certs:
|
|
||||||
description:
|
|
||||||
- Whether calls to the ACME directory will validate TLS certificates.
|
|
||||||
- "*Warning:* Should *only ever* be set to C(no) for testing purposes,
|
|
||||||
for example when testing against a local Pebble server."
|
|
||||||
type: bool
|
|
||||||
default: yes
|
|
||||||
version_added: "2.5"
|
|
||||||
select_crypto_backend:
|
|
||||||
description:
|
|
||||||
- Determines which crypto backend to use.
|
|
||||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to
|
|
||||||
C(openssl).
|
|
||||||
- If set to C(openssl), will try to use the C(openssl) binary.
|
|
||||||
- If set to C(cryptography), will try to use the
|
|
||||||
L(cryptography,https://cryptography.io/) library.
|
|
||||||
type: str
|
|
||||||
default: auto
|
|
||||||
choices: [ auto, cryptography, openssl ]
|
|
||||||
version_added: "2.7"
|
|
||||||
'''
|
|
@ -1,43 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright (c), Entrust Datacard Corporation, 2019
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleDocFragment(object):
|
|
||||||
|
|
||||||
# Plugin options for Entrust Certificate Services (ECS) credentials
|
|
||||||
DOCUMENTATION = r'''
|
|
||||||
options:
|
|
||||||
entrust_api_user:
|
|
||||||
description:
|
|
||||||
- The username for authentication to the Entrust Certificate Services (ECS) API.
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
entrust_api_key:
|
|
||||||
description:
|
|
||||||
- The key (password) for authentication to the Entrust Certificate Services (ECS) API.
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
entrust_api_client_cert_path:
|
|
||||||
description:
|
|
||||||
- The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
|
|
||||||
type: path
|
|
||||||
required: true
|
|
||||||
entrust_api_client_cert_key_path:
|
|
||||||
description:
|
|
||||||
- The path to the key for the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
|
|
||||||
type: path
|
|
||||||
required: true
|
|
||||||
entrust_api_specification_path:
|
|
||||||
description:
|
|
||||||
- The path to the specification file defining the Entrust Certificate Services (ECS) API configuration.
|
|
||||||
- You can use this to keep a local copy of the specification to avoid downloading it every time the module is used.
|
|
||||||
type: path
|
|
||||||
default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
|
|
||||||
requirements:
|
|
||||||
- "PyYAML >= 3.11"
|
|
||||||
'''
|
|
@ -1,2 +0,0 @@
|
|||||||
shippable/cloud/group1
|
|
||||||
cloud/acme
|
|
@ -1,2 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- setup_acme
|
|
@ -1,244 +0,0 @@
|
|||||||
- name: Generate account key
|
|
||||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem
|
|
||||||
|
|
||||||
- name: Parse account key (to ease debugging some test failures)
|
|
||||||
command: openssl ec -in {{ output_dir }}/accountkey.pem -noout -text
|
|
||||||
|
|
||||||
- name: Do not try to create account
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
allow_creation: no
|
|
||||||
ignore_errors: yes
|
|
||||||
register: account_not_created
|
|
||||||
|
|
||||||
- name: Create it now (check mode, diff)
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
allow_creation: yes
|
|
||||||
terms_agreed: yes
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.org
|
|
||||||
check_mode: yes
|
|
||||||
diff: yes
|
|
||||||
register: account_created_check
|
|
||||||
|
|
||||||
- name: Create it now
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
allow_creation: yes
|
|
||||||
terms_agreed: yes
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.org
|
|
||||||
register: account_created
|
|
||||||
|
|
||||||
- name: Create it now (idempotent)
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
allow_creation: yes
|
|
||||||
terms_agreed: yes
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.org
|
|
||||||
register: account_created_idempotent
|
|
||||||
|
|
||||||
- name: Change email address (check mode, diff)
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/accountkey.pem') }}"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
# allow_creation: no
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.com
|
|
||||||
check_mode: yes
|
|
||||||
diff: yes
|
|
||||||
register: account_modified_check
|
|
||||||
|
|
||||||
- name: Change email address
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/accountkey.pem') }}"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
# allow_creation: no
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.com
|
|
||||||
register: account_modified
|
|
||||||
|
|
||||||
- name: Change email address (idempotent)
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
account_uri: "{{ account_created.account_uri }}"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
# allow_creation: no
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.com
|
|
||||||
register: account_modified_idempotent
|
|
||||||
|
|
||||||
- name: Cannot access account with wrong URI
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
account_uri: "{{ account_created.account_uri ~ '12345thisdoesnotexist' }}"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
contact: []
|
|
||||||
ignore_errors: yes
|
|
||||||
register: account_modified_wrong_uri
|
|
||||||
|
|
||||||
- name: Clear contact email addresses (check mode, diff)
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
# allow_creation: no
|
|
||||||
contact: []
|
|
||||||
check_mode: yes
|
|
||||||
diff: yes
|
|
||||||
register: account_modified_2_check
|
|
||||||
|
|
||||||
- name: Clear contact email addresses
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
# allow_creation: no
|
|
||||||
contact: []
|
|
||||||
register: account_modified_2
|
|
||||||
|
|
||||||
- name: Clear contact email addresses (idempotent)
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
# allow_creation: no
|
|
||||||
contact: []
|
|
||||||
register: account_modified_2_idempotent
|
|
||||||
|
|
||||||
- name: Generate new account key
|
|
||||||
command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/accountkey2.pem
|
|
||||||
|
|
||||||
- name: Parse account key (to ease debugging some test failures)
|
|
||||||
command: openssl ec -in {{ output_dir }}/accountkey2.pem -noout -text
|
|
||||||
|
|
||||||
- name: Change account key (check mode, diff)
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
new_account_key_src: "{{ output_dir }}/accountkey2.pem"
|
|
||||||
state: changed_key
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.com
|
|
||||||
check_mode: yes
|
|
||||||
diff: yes
|
|
||||||
register: account_change_key_check
|
|
||||||
|
|
||||||
- name: Change account key
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
new_account_key_src: "{{ output_dir }}/accountkey2.pem"
|
|
||||||
state: changed_key
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.com
|
|
||||||
register: account_change_key
|
|
||||||
|
|
||||||
- name: Deactivate account (check mode, diff)
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey2.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: absent
|
|
||||||
check_mode: yes
|
|
||||||
diff: yes
|
|
||||||
register: account_deactivate_check
|
|
||||||
|
|
||||||
- name: Deactivate account
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey2.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: absent
|
|
||||||
register: account_deactivate
|
|
||||||
|
|
||||||
- name: Deactivate account (idempotent)
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey2.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: absent
|
|
||||||
register: account_deactivate_idempotent
|
|
||||||
|
|
||||||
- name: Do not try to create account II
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey2.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
allow_creation: no
|
|
||||||
ignore_errors: yes
|
|
||||||
register: account_not_created_2
|
|
||||||
|
|
||||||
- name: Do not try to create account III
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
allow_creation: no
|
|
||||||
ignore_errors: yes
|
|
||||||
register: account_not_created_3
|
|
@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
- block:
|
|
||||||
- name: Running tests with OpenSSL backend
|
|
||||||
include_tasks: impl.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: openssl
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
|
||||||
|
|
||||||
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
|
||||||
when: openssl_version.stdout is version('1.0.0', '>=')
|
|
||||||
|
|
||||||
- name: Remove output directory
|
|
||||||
file:
|
|
||||||
path: "{{ output_dir }}"
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Re-create output directory
|
|
||||||
file:
|
|
||||||
path: "{{ output_dir }}"
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: Running tests with cryptography backend
|
|
||||||
include_tasks: impl.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: cryptography
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
|
||||||
|
|
||||||
when: cryptography_version.stdout is version('1.5', '>=')
|
|
@ -1,129 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Validate that account wasn't created in the first step
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_not_created is failed
|
|
||||||
- account_not_created.msg == 'Account does not exist or is deactivated.'
|
|
||||||
|
|
||||||
- name: Validate that account was created in the second step (check mode)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_created_check is changed
|
|
||||||
- account_created_check.account_uri is none
|
|
||||||
- "'diff' in account_created_check"
|
|
||||||
- "account_created_check.diff.before == {}"
|
|
||||||
- "'after' in account_created_check.diff"
|
|
||||||
- account_created_check.diff.after.contact | length == 1
|
|
||||||
- account_created_check.diff.after.contact[0] == 'mailto:example@example.org'
|
|
||||||
|
|
||||||
- name: Validate that account was created in the second step
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_created is changed
|
|
||||||
- account_created.account_uri is not none
|
|
||||||
|
|
||||||
- name: Validate that account was created in the second step (idempotency)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_created_idempotent is not changed
|
|
||||||
- account_created_idempotent.account_uri is not none
|
|
||||||
|
|
||||||
- name: Validate that email address was changed (check mode)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_modified_check is changed
|
|
||||||
- account_modified_check.account_uri is not none
|
|
||||||
- "'diff' in account_modified_check"
|
|
||||||
- account_modified_check.diff.before.contact | length == 1
|
|
||||||
- account_modified_check.diff.before.contact[0] == 'mailto:example@example.org'
|
|
||||||
- account_modified_check.diff.after.contact | length == 1
|
|
||||||
- account_modified_check.diff.after.contact[0] == 'mailto:example@example.com'
|
|
||||||
|
|
||||||
- name: Validate that email address was changed
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_modified is changed
|
|
||||||
- account_modified.account_uri is not none
|
|
||||||
|
|
||||||
- name: Validate that email address was not changed a second time (idempotency)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_modified_idempotent is not changed
|
|
||||||
- account_modified_idempotent.account_uri is not none
|
|
||||||
|
|
||||||
- name: Make sure that with the wrong account URI, the account cannot be changed
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_modified_wrong_uri is failed
|
|
||||||
|
|
||||||
- name: Validate that email address was cleared (check mode)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_modified_2_check is changed
|
|
||||||
- account_modified_2_check.account_uri is not none
|
|
||||||
- "'diff' in account_modified_2_check"
|
|
||||||
- account_modified_2_check.diff.before.contact | length == 1
|
|
||||||
- account_modified_2_check.diff.before.contact[0] == 'mailto:example@example.com'
|
|
||||||
- account_modified_2_check.diff.after.contact | length == 0
|
|
||||||
|
|
||||||
- name: Validate that email address was cleared
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_modified_2 is changed
|
|
||||||
- account_modified_2.account_uri is not none
|
|
||||||
|
|
||||||
- name: Validate that email address was not cleared a second time (idempotency)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_modified_2_idempotent is not changed
|
|
||||||
- account_modified_2_idempotent.account_uri is not none
|
|
||||||
|
|
||||||
- name: Validate that the account key was changed (check mode)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_change_key_check is changed
|
|
||||||
- account_change_key_check.account_uri is not none
|
|
||||||
- "'diff' in account_change_key_check"
|
|
||||||
- account_change_key_check.diff.before.public_account_key != account_change_key_check.diff.after.public_account_key
|
|
||||||
|
|
||||||
- name: Validate that the account key was changed
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_change_key is changed
|
|
||||||
- account_change_key.account_uri is not none
|
|
||||||
|
|
||||||
- name: Validate that the account was deactivated (check mode)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_deactivate_check is changed
|
|
||||||
- account_deactivate_check.account_uri is not none
|
|
||||||
- "'diff' in account_deactivate_check"
|
|
||||||
- "account_deactivate_check.diff.before != {}"
|
|
||||||
- "account_deactivate_check.diff.after == {}"
|
|
||||||
|
|
||||||
- name: Validate that the account was deactivated
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_deactivate is changed
|
|
||||||
- account_deactivate.account_uri is not none
|
|
||||||
|
|
||||||
- name: Validate that the account was really deactivated (idempotency)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_deactivate_idempotent is not changed
|
|
||||||
# The next condition should be true for all conforming ACME servers.
|
|
||||||
# In case it is not true, it could be both an error in acme_account
|
|
||||||
# and in the ACME server.
|
|
||||||
- account_deactivate_idempotent.account_uri is none
|
|
||||||
|
|
||||||
- name: Validate that the account is gone (new account key)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_not_created_2 is failed
|
|
||||||
- account_not_created_2.msg == 'Account does not exist or is deactivated.'
|
|
||||||
|
|
||||||
- name: Validate that the account is gone (old account key)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_not_created_3 is failed
|
|
||||||
- account_not_created_3.msg == 'Account does not exist or is deactivated.'
|
|
@ -1,2 +0,0 @@
|
|||||||
shippable/cloud/group1
|
|
||||||
cloud/acme
|
|
@ -1,2 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- setup_acme
|
|
@ -1,82 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Generate account key
|
|
||||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem
|
|
||||||
|
|
||||||
- name: Generate second account key
|
|
||||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey2.pem
|
|
||||||
|
|
||||||
- name: Parse account key (to ease debugging some test failures)
|
|
||||||
command: openssl ec -in {{ output_dir }}/accountkey.pem -noout -text
|
|
||||||
|
|
||||||
- name: Check that account does not exist
|
|
||||||
acme_account_info:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
register: account_not_created
|
|
||||||
|
|
||||||
- name: Create it now
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
allow_creation: yes
|
|
||||||
terms_agreed: yes
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.org
|
|
||||||
|
|
||||||
- name: Check that account exists
|
|
||||||
acme_account_info:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
register: account_created
|
|
||||||
|
|
||||||
- name: Clear email address
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/accountkey.pem') }}"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
state: present
|
|
||||||
allow_creation: no
|
|
||||||
contact: []
|
|
||||||
|
|
||||||
- name: Check that account was modified
|
|
||||||
acme_account_info:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
account_uri: "{{ account_created.account_uri }}"
|
|
||||||
register: account_modified
|
|
||||||
|
|
||||||
- name: Check with wrong account URI
|
|
||||||
acme_account_info:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
account_uri: "{{ account_created.account_uri }}test1234doesnotexists"
|
|
||||||
register: account_not_exist
|
|
||||||
|
|
||||||
- name: Check with wrong account key
|
|
||||||
acme_account_info:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey2.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
account_uri: "{{ account_created.account_uri }}"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: account_wrong_key
|
|
@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
- block:
|
|
||||||
- name: Running tests with OpenSSL backend
|
|
||||||
include_tasks: impl.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: openssl
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
|
||||||
|
|
||||||
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
|
||||||
when: openssl_version.stdout is version('1.0.0', '>=')
|
|
||||||
|
|
||||||
- name: Remove output directory
|
|
||||||
file:
|
|
||||||
path: "{{ output_dir }}"
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Re-create output directory
|
|
||||||
file:
|
|
||||||
path: "{{ output_dir }}"
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: Running tests with cryptography backend
|
|
||||||
include_tasks: impl.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: cryptography
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
|
||||||
|
|
||||||
when: cryptography_version.stdout is version('1.5', '>=')
|
|
@ -1,40 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Validate that account wasn't there
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- not account_not_created.exists
|
|
||||||
- account_not_created.account_uri is none
|
|
||||||
- "'account' not in account_not_created"
|
|
||||||
|
|
||||||
- name: Validate that account was created
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_created.exists
|
|
||||||
- account_created.account_uri is not none
|
|
||||||
- "'account' in account_created"
|
|
||||||
- "'contact' in account_created.account"
|
|
||||||
- "'public_account_key' in account_created.account"
|
|
||||||
- account_created.account.contact | length == 1
|
|
||||||
- "account_created.account.contact[0] == 'mailto:example@example.org'"
|
|
||||||
|
|
||||||
- name: Validate that account email was removed
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_modified.exists
|
|
||||||
- account_modified.account_uri is not none
|
|
||||||
- "'account' in account_modified"
|
|
||||||
- "'contact' in account_modified.account"
|
|
||||||
- "'public_account_key' in account_modified.account"
|
|
||||||
- account_modified.account.contact | length == 0
|
|
||||||
|
|
||||||
- name: Validate that account does not exist with wrong account URI
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- not account_not_exist.exists
|
|
||||||
- account_not_exist.account_uri is none
|
|
||||||
- "'account' not in account_not_exist"
|
|
||||||
|
|
||||||
- name: Validate that account cannot be accessed with wrong key
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_wrong_key is failed
|
|
@ -1,2 +0,0 @@
|
|||||||
shippable/cloud/group1
|
|
||||||
cloud/acme
|
|
@ -1,2 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- setup_acme
|
|
@ -1,451 +0,0 @@
|
|||||||
---
|
|
||||||
## SET UP ACCOUNT KEYS ########################################################################
|
|
||||||
- name: Create ECC256 account key
|
|
||||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem
|
|
||||||
- name: Create ECC384 account key
|
|
||||||
command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem
|
|
||||||
- name: Create RSA-2048 account key
|
|
||||||
command: openssl genrsa -out {{ output_dir }}/account-rsa2048.pem 2048
|
|
||||||
## SET UP ACCOUNTS ############################################################################
|
|
||||||
- name: Make sure ECC256 account hasn't been created yet
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
|
||||||
state: absent
|
|
||||||
- name: Create ECC384 account
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec384.pem') }}"
|
|
||||||
state: present
|
|
||||||
allow_creation: yes
|
|
||||||
terms_agreed: yes
|
|
||||||
contact:
|
|
||||||
- mailto:example@example.org
|
|
||||||
- mailto:example@example.com
|
|
||||||
- name: Create RSA-2048 account
|
|
||||||
acme_account:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/account-rsa2048.pem"
|
|
||||||
state: present
|
|
||||||
allow_creation: yes
|
|
||||||
terms_agreed: yes
|
|
||||||
contact: []
|
|
||||||
## OBTAIN CERTIFICATES ########################################################################
|
|
||||||
- name: Obtain cert 1
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 1
|
|
||||||
certificate_name: cert-1
|
|
||||||
key_type: rsa
|
|
||||||
rsa_bits: 2048
|
|
||||||
subject_alt_name: "DNS:example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec256
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
retrieve_all_alternates: yes
|
|
||||||
acme_expected_root_number: 1
|
|
||||||
select_chain:
|
|
||||||
- test_certificates: last
|
|
||||||
issuer: "{{ acme_roots[1].subject }}"
|
|
||||||
- name: Store obtain results for cert 1
|
|
||||||
set_fact:
|
|
||||||
cert_1_obtain_results: "{{ certificate_obtain_result }}"
|
|
||||||
cert_1_alternate: "{{ 1 if select_crypto_backend == 'cryptography' else 0 }}"
|
|
||||||
- name: Obtain cert 2
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 2
|
|
||||||
certificate_name: cert-2
|
|
||||||
key_type: ec256
|
|
||||||
subject_alt_name: "DNS:*.example.com,DNS:example.com"
|
|
||||||
subject_alt_name_critical: yes
|
|
||||||
account_key: account-ec384
|
|
||||||
challenge: dns-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
acme_expected_root_number: 0
|
|
||||||
retrieve_all_alternates: yes
|
|
||||||
select_chain:
|
|
||||||
# All intermediates have the same subject, so always the first
|
|
||||||
# chain will be found, and we need a second condition to make sure
|
|
||||||
# that the first condition actually works. (The second condition
|
|
||||||
# has been tested above.)
|
|
||||||
- test_certificates: all
|
|
||||||
subject: "{{ acme_intermediates[0].subject }}"
|
|
||||||
- test_certificates: all
|
|
||||||
issuer: "{{ acme_roots[2].subject }}"
|
|
||||||
- name: Store obtain results for cert 2
|
|
||||||
set_fact:
|
|
||||||
cert_2_obtain_results: "{{ certificate_obtain_result }}"
|
|
||||||
cert_2_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
|
||||||
- name: Obtain cert 3
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 3
|
|
||||||
certificate_name: cert-3
|
|
||||||
key_type: ec384
|
|
||||||
subject_alt_name: "DNS:*.example.com,DNS:example.org,DNS:t1.example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa2048.pem') }}"
|
|
||||||
challenge: dns-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
acme_expected_root_number: 0
|
|
||||||
retrieve_all_alternates: yes
|
|
||||||
select_chain:
|
|
||||||
- test_certificates: last
|
|
||||||
subject: "{{ acme_roots[1].subject }}"
|
|
||||||
- name: Store obtain results for cert 3
|
|
||||||
set_fact:
|
|
||||||
cert_3_obtain_results: "{{ certificate_obtain_result }}"
|
|
||||||
cert_3_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
|
||||||
- name: Obtain cert 4
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 4
|
|
||||||
certificate_name: cert-4
|
|
||||||
key_type: rsa
|
|
||||||
rsa_bits: 2048
|
|
||||||
subject_alt_name: "DNS:example.com,DNS:t1.example.com,DNS:test.t2.example.com,DNS:example.org,DNS:test.example.org"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-rsa2048
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: yes
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
acme_expected_root_number: 2
|
|
||||||
select_chain:
|
|
||||||
- test_certificates: last
|
|
||||||
issuer: "{{ acme_roots[2].subject }}"
|
|
||||||
- test_certificates: last
|
|
||||||
issuer: "{{ acme_roots[1].subject }}"
|
|
||||||
- name: Store obtain results for cert 4
|
|
||||||
set_fact:
|
|
||||||
cert_4_obtain_results: "{{ certificate_obtain_result }}"
|
|
||||||
cert_4_alternate: "{{ 2 if select_crypto_backend == 'cryptography' else 0 }}"
|
|
||||||
- name: Obtain cert 5
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 5, Iteration 1/4
|
|
||||||
certificate_name: cert-5
|
|
||||||
key_type: ec521
|
|
||||||
subject_alt_name: "DNS:t2.example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec384
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: yes
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
- name: Store obtain results for cert 5a
|
|
||||||
set_fact:
|
|
||||||
cert_5a_obtain_results: "{{ certificate_obtain_result }}"
|
|
||||||
cert_5_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
|
||||||
- name: Obtain cert 5 (should not, since already there and valid for more than 10 days)
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 5, Iteration 2/4
|
|
||||||
certificate_name: cert-5
|
|
||||||
key_type: ec521
|
|
||||||
subject_alt_name: "DNS:t2.example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec384
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
- name: Store obtain results for cert 5b
|
|
||||||
set_fact:
|
|
||||||
cert_5_recreate_1: "{{ challenge_data is changed }}"
|
|
||||||
- name: Obtain cert 5 (should again by less days)
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 5, Iteration 3/4
|
|
||||||
certificate_name: cert-5
|
|
||||||
key_type: ec521
|
|
||||||
subject_alt_name: "DNS:t2.example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec384
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: yes
|
|
||||||
remaining_days: 1000
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
- name: Store obtain results for cert 5c
|
|
||||||
set_fact:
|
|
||||||
cert_5_recreate_2: "{{ challenge_data is changed }}"
|
|
||||||
cert_5c_obtain_results: "{{ certificate_obtain_result }}"
|
|
||||||
- name: Obtain cert 5 (should again by force)
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 5, Iteration 4/4
|
|
||||||
certificate_name: cert-5
|
|
||||||
key_type: ec521
|
|
||||||
subject_alt_name: "DNS:t2.example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec384.pem') }}"
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: no
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: yes
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: no
|
|
||||||
account_email: ""
|
|
||||||
- name: Store obtain results for cert 5d
|
|
||||||
set_fact:
|
|
||||||
cert_5_recreate_3: "{{ challenge_data is changed }}"
|
|
||||||
cert_5d_obtain_results: "{{ certificate_obtain_result }}"
|
|
||||||
- name: Obtain cert 6
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 6
|
|
||||||
certificate_name: cert-6
|
|
||||||
key_type: rsa
|
|
||||||
rsa_bits: 2048
|
|
||||||
subject_alt_name: "DNS:example.org"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec256
|
|
||||||
challenge: tls-alpn-01
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
acme_expected_root_number: 0
|
|
||||||
select_chain:
|
|
||||||
# All intermediates have the same subject key identifier, so always
|
|
||||||
# the first chain will be found, and we need a second condition to
|
|
||||||
# make sure that the first condition actually works. (The second
|
|
||||||
# condition has been tested above.)
|
|
||||||
- test_certificates: last
|
|
||||||
subject_key_identifier: "{{ acme_intermediates[0].subject_key_identifier }}"
|
|
||||||
- test_certificates: last
|
|
||||||
issuer: "{{ acme_roots[1].subject }}"
|
|
||||||
- name: Store obtain results for cert 6
|
|
||||||
set_fact:
|
|
||||||
cert_6_obtain_results: "{{ certificate_obtain_result }}"
|
|
||||||
cert_6_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
|
||||||
- name: Obtain cert 7
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 7
|
|
||||||
certificate_name: cert-7
|
|
||||||
key_type: rsa
|
|
||||||
rsa_bits: 2048
|
|
||||||
subject_alt_name:
|
|
||||||
- "IP:127.0.0.1"
|
|
||||||
# - "IP:::1"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec256
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
acme_expected_root_number: 2
|
|
||||||
select_chain:
|
|
||||||
- test_certificates: last
|
|
||||||
authority_key_identifier: "{{ acme_roots[2].subject_key_identifier }}"
|
|
||||||
- name: Store obtain results for cert 7
|
|
||||||
set_fact:
|
|
||||||
cert_7_obtain_results: "{{ certificate_obtain_result }}"
|
|
||||||
cert_7_alternate: "{{ 2 if select_crypto_backend == 'cryptography' else 0 }}"
|
|
||||||
- name: Obtain cert 8
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 8
|
|
||||||
certificate_name: cert-8
|
|
||||||
key_type: rsa
|
|
||||||
rsa_bits: 2048
|
|
||||||
subject_alt_name:
|
|
||||||
- "IP:127.0.0.1"
|
|
||||||
# IPv4 only since our test validation server doesn't work
|
|
||||||
# with IPv6 (thanks to Python's socketserver).
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec256
|
|
||||||
challenge: tls-alpn-01
|
|
||||||
challenge_alpn_tls: acme_challenge_cert_helper
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
- name: Store obtain results for cert 8
|
|
||||||
set_fact:
|
|
||||||
cert_8_obtain_results: "{{ certificate_obtain_result }}"
|
|
||||||
cert_8_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
|
||||||
## DISSECT CERTIFICATES #######################################################################
|
|
||||||
# Make sure certificates are valid. Root certificate for Pebble equals the chain certificate.
|
|
||||||
- name: Verifying cert 1
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-1-root.pem" -untrusted "{{ output_dir }}/cert-1-chain.pem" "{{ output_dir }}/cert-1.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_1_valid
|
|
||||||
- name: Verifying cert 2
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-2-root.pem" -untrusted "{{ output_dir }}/cert-2-chain.pem" "{{ output_dir }}/cert-2.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_2_valid
|
|
||||||
- name: Verifying cert 3
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-3-root.pem" -untrusted "{{ output_dir }}/cert-3-chain.pem" "{{ output_dir }}/cert-3.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_3_valid
|
|
||||||
- name: Verifying cert 4
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-4-root.pem" -untrusted "{{ output_dir }}/cert-4-chain.pem" "{{ output_dir }}/cert-4.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_4_valid
|
|
||||||
- name: Verifying cert 5
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-5-root.pem" -untrusted "{{ output_dir }}/cert-5-chain.pem" "{{ output_dir }}/cert-5.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_5_valid
|
|
||||||
- name: Verifying cert 6
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-6-root.pem" -untrusted "{{ output_dir }}/cert-6-chain.pem" "{{ output_dir }}/cert-6.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_6_valid
|
|
||||||
- name: Verifying cert 7
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-7-root.pem" -untrusted "{{ output_dir }}/cert-7-chain.pem" "{{ output_dir }}/cert-7.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_7_valid
|
|
||||||
- name: Verifying cert 8
|
|
||||||
command: openssl verify -CAfile "{{ output_dir }}/cert-8-root.pem" -untrusted "{{ output_dir }}/cert-8-chain.pem" "{{ output_dir }}/cert-8.pem"
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_8_valid
|
|
||||||
# Dump certificate info
|
|
||||||
- name: Dumping cert 1
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-1.pem" -noout -text
|
|
||||||
register: cert_1_text
|
|
||||||
- name: Dumping cert 2
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-2.pem" -noout -text
|
|
||||||
register: cert_2_text
|
|
||||||
- name: Dumping cert 3
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-3.pem" -noout -text
|
|
||||||
register: cert_3_text
|
|
||||||
- name: Dumping cert 4
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-4.pem" -noout -text
|
|
||||||
register: cert_4_text
|
|
||||||
- name: Dumping cert 5
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-5.pem" -noout -text
|
|
||||||
register: cert_5_text
|
|
||||||
- name: Dumping cert 6
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-6.pem" -noout -text
|
|
||||||
register: cert_6_text
|
|
||||||
- name: Dumping cert 7
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-7.pem" -noout -text
|
|
||||||
register: cert_7_text
|
|
||||||
- name: Dumping cert 8
|
|
||||||
command: openssl x509 -in "{{ output_dir }}/cert-8.pem" -noout -text
|
|
||||||
register: cert_8_text
|
|
||||||
# Dump certificate info
|
|
||||||
- name: Dumping cert 1
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: "{{ output_dir }}/cert-1.pem"
|
|
||||||
register: cert_1_info
|
|
||||||
- name: Dumping cert 2
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: "{{ output_dir }}/cert-2.pem"
|
|
||||||
register: cert_2_info
|
|
||||||
- name: Dumping cert 3
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: "{{ output_dir }}/cert-3.pem"
|
|
||||||
register: cert_3_info
|
|
||||||
- name: Dumping cert 4
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: "{{ output_dir }}/cert-4.pem"
|
|
||||||
register: cert_4_info
|
|
||||||
- name: Dumping cert 5
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: "{{ output_dir }}/cert-5.pem"
|
|
||||||
register: cert_5_info
|
|
||||||
- name: Dumping cert 6
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: "{{ output_dir }}/cert-6.pem"
|
|
||||||
register: cert_6_info
|
|
||||||
- name: Dumping cert 7
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: "{{ output_dir }}/cert-7.pem"
|
|
||||||
register: cert_7_info
|
|
||||||
- name: Dumping cert 8
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: "{{ output_dir }}/cert-8.pem"
|
|
||||||
register: cert_8_info
|
|
||||||
## GET ACCOUNT ORDERS #########################################################################
|
|
||||||
- name: Don't retrieve orders
|
|
||||||
acme_account_info:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
retrieve_orders: ignore
|
|
||||||
register: account_orders_not
|
|
||||||
- name: Retrieve orders as URL list (1/2)
|
|
||||||
acme_account_info:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
retrieve_orders: url_list
|
|
||||||
register: account_orders_urls
|
|
||||||
- name: Retrieve orders as URL list (2/2)
|
|
||||||
acme_account_info:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/account-ec384.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
retrieve_orders: url_list
|
|
||||||
register: account_orders_urls2
|
|
||||||
- name: Retrieve orders as object list (1/2)
|
|
||||||
acme_account_info:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
retrieve_orders: object_list
|
|
||||||
register: account_orders_full
|
|
||||||
- name: Retrieve orders as object list (2/2)
|
|
||||||
acme_account_info:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/account-ec384.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
retrieve_orders: object_list
|
|
||||||
register: account_orders_full2
|
|
@ -1,102 +0,0 @@
|
|||||||
---
|
|
||||||
- block:
|
|
||||||
- name: Obtain root and intermediate certificates
|
|
||||||
get_url:
|
|
||||||
url: "http://{{ acme_host }}:5000/{{ item.0 }}-certificate-for-ca/{{ item.1 }}"
|
|
||||||
dest: "{{ output_dir }}/acme-{{ item.0 }}-{{ item.1 }}.pem"
|
|
||||||
loop: "{{ query('nested', types, root_numbers) }}"
|
|
||||||
|
|
||||||
- name: Analyze root certificates
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: "{{ output_dir }}/acme-root-{{ item }}.pem"
|
|
||||||
loop: "{{ root_numbers }}"
|
|
||||||
register: acme_roots
|
|
||||||
|
|
||||||
- name: Analyze intermediate certificates
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: "{{ output_dir }}/acme-intermediate-{{ item }}.pem"
|
|
||||||
loop: "{{ root_numbers }}"
|
|
||||||
register: acme_intermediates
|
|
||||||
|
|
||||||
- set_fact:
|
|
||||||
x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}"
|
|
||||||
y__: "{{ lookup('file', output_dir ~ '/acme-root-' ~ item.item ~ '.pem', rstrip=False) }}"
|
|
||||||
loop: "{{ acme_roots.results }}"
|
|
||||||
register: acme_roots_tmp
|
|
||||||
|
|
||||||
- set_fact:
|
|
||||||
x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}"
|
|
||||||
y__: "{{ lookup('file', output_dir ~ '/acme-intermediate-' ~ item.item ~ '.pem', rstrip=False) }}"
|
|
||||||
loop: "{{ acme_intermediates.results }}"
|
|
||||||
register: acme_intermediates_tmp
|
|
||||||
|
|
||||||
- set_fact:
|
|
||||||
acme_roots: "{{ acme_roots_tmp.results | map(attribute='ansible_facts.x__') | list }}"
|
|
||||||
acme_root_certs: "{{ acme_roots_tmp.results | map(attribute='ansible_facts.y__') | list }}"
|
|
||||||
acme_intermediates: "{{ acme_intermediates_tmp.results | map(attribute='ansible_facts.x__') | list }}"
|
|
||||||
acme_intermediate_certs: "{{ acme_intermediates_tmp.results | map(attribute='ansible_facts.y__') | list }}"
|
|
||||||
|
|
||||||
vars:
|
|
||||||
types:
|
|
||||||
- root
|
|
||||||
- intermediate
|
|
||||||
root_numbers:
|
|
||||||
# The number 3 comes from here: https://github.com/ansible/acme-test-container/blob/master/run.sh#L12
|
|
||||||
- 0
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
- 3
|
|
||||||
interesting_keys:
|
|
||||||
- authority_key_identifier
|
|
||||||
- subject_key_identifier
|
|
||||||
- issuer
|
|
||||||
- subject
|
|
||||||
#- serial_number
|
|
||||||
#- public_key_fingerprints
|
|
||||||
|
|
||||||
- name: ACME root certificate info
|
|
||||||
debug:
|
|
||||||
var: acme_roots
|
|
||||||
|
|
||||||
#- name: ACME root certificates as PEM
|
|
||||||
# debug:
|
|
||||||
# var: acme_root_certs
|
|
||||||
|
|
||||||
- name: ACME intermediate certificate info
|
|
||||||
debug:
|
|
||||||
var: acme_intermediates
|
|
||||||
|
|
||||||
#- name: ACME intermediate certificates as PEM
|
|
||||||
# debug:
|
|
||||||
# var: acme_intermediate_certs
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: Running tests with OpenSSL backend
|
|
||||||
include_tasks: impl.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: openssl
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
|
||||||
|
|
||||||
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
|
||||||
when: openssl_version.stdout is version('1.0.0', '>=')
|
|
||||||
|
|
||||||
- name: Remove output directory
|
|
||||||
file:
|
|
||||||
path: "{{ output_dir }}"
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Re-create output directory
|
|
||||||
file:
|
|
||||||
path: "{{ output_dir }}"
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: Running tests with cryptography backend
|
|
||||||
include_tasks: impl.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: cryptography
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
|
||||||
|
|
||||||
when: cryptography_version.stdout is version('1.5', '>=')
|
|
@ -1 +0,0 @@
|
|||||||
../../setup_acme/tasks/obtain-cert.yml
|
|
@ -1,144 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Check that certificate 1 is valid
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- cert_1_valid is not failed
|
|
||||||
- name: Check that certificate 1 contains correct SANs
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'DNS:example.com' in cert_1_text.stdout"
|
|
||||||
- name: Check that certificate 1 retrieval got all chains
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'all_chains' in cert_1_obtain_results"
|
|
||||||
- "cert_1_obtain_results.all_chains | length > 1"
|
|
||||||
- "'cert' in cert_1_obtain_results.all_chains[cert_1_alternate | int]"
|
|
||||||
- "'chain' in cert_1_obtain_results.all_chains[cert_1_alternate | int]"
|
|
||||||
- "'full_chain' in cert_1_obtain_results.all_chains[cert_1_alternate | int]"
|
|
||||||
- "lookup('file', output_dir ~ '/cert-1.pem', rstrip=False) == cert_1_obtain_results.all_chains[cert_1_alternate | int].cert"
|
|
||||||
- "lookup('file', output_dir ~ '/cert-1-chain.pem', rstrip=False) == cert_1_obtain_results.all_chains[cert_1_alternate | int].chain"
|
|
||||||
- "lookup('file', output_dir ~ '/cert-1-fullchain.pem', rstrip=False) == cert_1_obtain_results.all_chains[cert_1_alternate | int].full_chain"
|
|
||||||
|
|
||||||
- name: Check that certificate 2 is valid
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- cert_2_valid is not failed
|
|
||||||
- name: Check that certificate 2 contains correct SANs
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'DNS:*.example.com' in cert_2_text.stdout"
|
|
||||||
- "'DNS:example.com' in cert_2_text.stdout"
|
|
||||||
- name: Check that certificate 1 retrieval got all chains
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'all_chains' in cert_2_obtain_results"
|
|
||||||
- "cert_2_obtain_results.all_chains | length > 1"
|
|
||||||
- "'cert' in cert_2_obtain_results.all_chains[cert_2_alternate | int]"
|
|
||||||
- "'chain' in cert_2_obtain_results.all_chains[cert_2_alternate | int]"
|
|
||||||
- "'full_chain' in cert_2_obtain_results.all_chains[cert_2_alternate | int]"
|
|
||||||
- "lookup('file', output_dir ~ '/cert-2.pem', rstrip=False) == cert_2_obtain_results.all_chains[cert_2_alternate | int].cert"
|
|
||||||
- "lookup('file', output_dir ~ '/cert-2-chain.pem', rstrip=False) == cert_2_obtain_results.all_chains[cert_2_alternate | int].chain"
|
|
||||||
- "lookup('file', output_dir ~ '/cert-2-fullchain.pem', rstrip=False) == cert_2_obtain_results.all_chains[cert_2_alternate | int].full_chain"
|
|
||||||
|
|
||||||
- name: Check that certificate 3 is valid
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- cert_3_valid is not failed
|
|
||||||
- name: Check that certificate 3 contains correct SANs
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'DNS:*.example.com' in cert_3_text.stdout"
|
|
||||||
- "'DNS:example.org' in cert_3_text.stdout"
|
|
||||||
- "'DNS:t1.example.com' in cert_3_text.stdout"
|
|
||||||
- name: Check that certificate 1 retrieval got all chains
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'all_chains' in cert_3_obtain_results"
|
|
||||||
- "cert_3_obtain_results.all_chains | length > 1"
|
|
||||||
- "'cert' in cert_3_obtain_results.all_chains[cert_3_alternate | int]"
|
|
||||||
- "'chain' in cert_3_obtain_results.all_chains[cert_3_alternate | int]"
|
|
||||||
- "'full_chain' in cert_3_obtain_results.all_chains[cert_3_alternate | int]"
|
|
||||||
- "lookup('file', output_dir ~ '/cert-3.pem', rstrip=False) == cert_3_obtain_results.all_chains[cert_3_alternate | int].cert"
|
|
||||||
- "lookup('file', output_dir ~ '/cert-3-chain.pem', rstrip=False) == cert_3_obtain_results.all_chains[cert_3_alternate | int].chain"
|
|
||||||
- "lookup('file', output_dir ~ '/cert-3-fullchain.pem', rstrip=False) == cert_3_obtain_results.all_chains[cert_3_alternate | int].full_chain"
|
|
||||||
|
|
||||||
- name: Check that certificate 4 is valid
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- cert_4_valid is not failed
|
|
||||||
- name: Check that certificate 4 contains correct SANs
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'DNS:example.com' in cert_4_text.stdout"
|
|
||||||
- "'DNS:t1.example.com' in cert_4_text.stdout"
|
|
||||||
- "'DNS:test.t2.example.com' in cert_4_text.stdout"
|
|
||||||
- "'DNS:example.org' in cert_4_text.stdout"
|
|
||||||
- "'DNS:test.example.org' in cert_4_text.stdout"
|
|
||||||
- name: Check that certificate 4 retrieval did not get all chains
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'all_chains' not in cert_4_obtain_results"
|
|
||||||
|
|
||||||
- name: Check that certificate 5 is valid
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- cert_5_valid is not failed
|
|
||||||
- name: Check that certificate 5 contains correct SANs
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'DNS:t2.example.com' in cert_5_text.stdout"
|
|
||||||
- name: Check that certificate 5 was not recreated on the first try
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- cert_5_recreate_1 == False
|
|
||||||
- name: Check that certificate 5 was recreated on the second try
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- cert_5_recreate_2 == True
|
|
||||||
- name: Check that certificate 5 was recreated on the third try
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- cert_5_recreate_3 == True
|
|
||||||
|
|
||||||
- name: Check that certificate 6 is valid
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- cert_6_valid is not failed
|
|
||||||
- name: Check that certificate 6 contains correct SANs
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'DNS:example.org' in cert_6_text.stdout"
|
|
||||||
|
|
||||||
- name: Validate that orders were not retrieved
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'account' in account_orders_not"
|
|
||||||
- "'orders' not in account_orders_not"
|
|
||||||
|
|
||||||
- name: Validate that orders were retrieved as list of URLs (1/2)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'account' in account_orders_urls"
|
|
||||||
- "'orders' in account_orders_urls"
|
|
||||||
- "account_orders_urls.orders[0] is string"
|
|
||||||
|
|
||||||
- name: Validate that orders were retrieved as list of URLs (2/2)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'account' in account_orders_urls2"
|
|
||||||
- "'orders' in account_orders_urls2"
|
|
||||||
- "account_orders_urls2.orders[0] is string"
|
|
||||||
|
|
||||||
- name: Validate that orders were retrieved as list of objects (1/2)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'account' in account_orders_full"
|
|
||||||
- "'orders' in account_orders_full"
|
|
||||||
- "account_orders_full.orders[0].status is string"
|
|
||||||
|
|
||||||
- name: Validate that orders were retrieved as list of objects (2/2)
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'account' in account_orders_full2"
|
|
||||||
- "'orders' in account_orders_full2"
|
|
||||||
- "account_orders_full2.orders[0].status is string"
|
|
@ -1,2 +0,0 @@
|
|||||||
shippable/cloud/group1
|
|
||||||
cloud/acme
|
|
@ -1,2 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- setup_acme
|
|
@ -1,89 +0,0 @@
|
|||||||
---
|
|
||||||
## SET UP ACCOUNT KEYS ########################################################################
|
|
||||||
- name: Create ECC256 account key
|
|
||||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem
|
|
||||||
- name: Create ECC384 account key
|
|
||||||
command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem
|
|
||||||
- name: Create RSA-2048 account key
|
|
||||||
command: openssl genrsa -out {{ output_dir }}/account-rsa2048.pem 2048
|
|
||||||
## CREATE ACCOUNTS AND OBTAIN CERTIFICATES ####################################################
|
|
||||||
- name: Obtain cert 1
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 1 for revocation
|
|
||||||
certificate_name: cert-1
|
|
||||||
key_type: rsa
|
|
||||||
rsa_bits: 2048
|
|
||||||
subject_alt_name: "DNS:example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec256.pem') }}"
|
|
||||||
challenge: http-01
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
- name: Obtain cert 2
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 2 for revocation
|
|
||||||
certificate_name: cert-2
|
|
||||||
key_type: ec256
|
|
||||||
subject_alt_name: "DNS:*.example.com"
|
|
||||||
subject_alt_name_critical: yes
|
|
||||||
account_key: account-ec384
|
|
||||||
challenge: dns-01
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: yes
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
- name: Obtain cert 3
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
certgen_title: Certificate 3 for revocation
|
|
||||||
certificate_name: cert-3
|
|
||||||
key_type: ec384
|
|
||||||
subject_alt_name: "DNS:t1.example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-rsa2048
|
|
||||||
challenge: dns-01
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
## REVOKE CERTIFICATES ########################################################################
|
|
||||||
- name: Revoke certificate 1 via account key
|
|
||||||
acme_certificate_revoke:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
|
||||||
certificate: "{{ output_dir }}/cert-1.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_1_revoke
|
|
||||||
- name: Revoke certificate 2 via certificate private key
|
|
||||||
acme_certificate_revoke:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
private_key_src: "{{ output_dir }}/cert-2.key"
|
|
||||||
certificate: "{{ output_dir }}/cert-2.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_2_revoke
|
|
||||||
- name: Revoke certificate 3 via account key (fullchain)
|
|
||||||
acme_certificate_revoke:
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa2048.pem') }}"
|
|
||||||
certificate: "{{ output_dir }}/cert-3-fullchain.pem"
|
|
||||||
acme_version: 2
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
validate_certs: no
|
|
||||||
ignore_errors: yes
|
|
||||||
register: cert_3_revoke
|
|
@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
- block:
|
|
||||||
- name: Running tests with OpenSSL backend
|
|
||||||
include_tasks: impl.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: openssl
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
|
||||||
|
|
||||||
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
|
||||||
when: openssl_version.stdout is version('1.0.0', '>=')
|
|
||||||
|
|
||||||
- name: Remove output directory
|
|
||||||
file:
|
|
||||||
path: "{{ output_dir }}"
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Re-create output directory
|
|
||||||
file:
|
|
||||||
path: "{{ output_dir }}"
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: Running tests with cryptography backend
|
|
||||||
include_tasks: impl.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: cryptography
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
|
||||||
|
|
||||||
when: cryptography_version.stdout is version('1.5', '>=')
|
|
@ -1 +0,0 @@
|
|||||||
../../setup_acme/tasks/obtain-cert.yml
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Check that certificate 1 was revoked
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- cert_1_revoke is changed
|
|
||||||
- cert_1_revoke is not failed
|
|
||||||
- name: Check that certificate 2 was revoked
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- cert_2_revoke is changed
|
|
||||||
- cert_2_revoke is not failed
|
|
||||||
- name: Check that certificate 3 was revoked
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- cert_3_revoke is changed
|
|
||||||
- cert_3_revoke is not failed
|
|
@ -1,2 +0,0 @@
|
|||||||
shippable/cloud/group1
|
|
||||||
cloud/acme
|
|
@ -1,2 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- setup_acme
|
|
@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
- block:
|
|
||||||
- name: Create ECC256 account key
|
|
||||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem
|
|
||||||
- name: Obtain cert 1
|
|
||||||
include_tasks: obtain-cert.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: auto
|
|
||||||
certgen_title: Certificate 1
|
|
||||||
certificate_name: cert-1
|
|
||||||
key_type: rsa
|
|
||||||
rsa_bits: 2048
|
|
||||||
subject_alt_name: "DNS:example.com"
|
|
||||||
subject_alt_name_critical: no
|
|
||||||
account_key: account-ec256
|
|
||||||
challenge: tls-alpn-01
|
|
||||||
challenge_alpn_tls: acme_challenge_cert_helper
|
|
||||||
modify_account: yes
|
|
||||||
deactivate_authzs: no
|
|
||||||
force: no
|
|
||||||
remaining_days: 10
|
|
||||||
terms_agreed: yes
|
|
||||||
account_email: "example@example.org"
|
|
||||||
|
|
||||||
when: openssl_version.stdout is version('1.0.0', '>=') or cryptography_version.stdout is version('1.5', '>=')
|
|
@ -1 +0,0 @@
|
|||||||
../../setup_acme/tasks/obtain-cert.yml
|
|
@ -1,2 +0,0 @@
|
|||||||
shippable/cloud/group1
|
|
||||||
cloud/acme
|
|
@ -1,2 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- setup_acme
|
|
@ -1,151 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Generate account key
|
|
||||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem
|
|
||||||
|
|
||||||
- name: Parse account key (to ease debugging some test failures)
|
|
||||||
command: openssl ec -in {{ output_dir }}/accountkey.pem -noout -text
|
|
||||||
|
|
||||||
- name: Get directory
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
acme_version: 2
|
|
||||||
validate_certs: no
|
|
||||||
method: directory-only
|
|
||||||
register: directory
|
|
||||||
- debug: var=directory
|
|
||||||
|
|
||||||
- name: Create an account
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
acme_version: 2
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
url: "{{ directory.directory.newAccount}}"
|
|
||||||
method: post
|
|
||||||
content: '{"termsOfServiceAgreed":true}'
|
|
||||||
register: account_creation
|
|
||||||
# account_creation.headers.location contains the account URI
|
|
||||||
# if creation was successful
|
|
||||||
- debug: var=account_creation
|
|
||||||
|
|
||||||
- name: Get account information
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
acme_version: 2
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ account_creation.headers.location }}"
|
|
||||||
method: get
|
|
||||||
register: account_get
|
|
||||||
- debug: var=account_get
|
|
||||||
|
|
||||||
- name: Update account contacts
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
acme_version: 2
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ account_creation.headers.location }}"
|
|
||||||
method: post
|
|
||||||
content: '{{ account_info | to_json }}'
|
|
||||||
vars:
|
|
||||||
account_info:
|
|
||||||
# For valid values, see
|
|
||||||
# https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3
|
|
||||||
contact:
|
|
||||||
- mailto:me@example.com
|
|
||||||
register: account_update
|
|
||||||
- debug: var=account_update
|
|
||||||
|
|
||||||
- name: Create certificate order
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
acme_version: 2
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ directory.directory.newOrder }}"
|
|
||||||
method: post
|
|
||||||
content: '{{ create_order | to_json }}'
|
|
||||||
vars:
|
|
||||||
create_order:
|
|
||||||
# For valid values, see
|
|
||||||
# https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4 and
|
|
||||||
# https://www.rfc-editor.org/rfc/rfc8738.html
|
|
||||||
identifiers:
|
|
||||||
- type: dns
|
|
||||||
value: example.com
|
|
||||||
- type: dns
|
|
||||||
value: example.org
|
|
||||||
register: new_order
|
|
||||||
- debug: var=new_order
|
|
||||||
|
|
||||||
- name: Get order information
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
acme_version: 2
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ new_order.headers.location }}"
|
|
||||||
method: get
|
|
||||||
register: order
|
|
||||||
- debug: var=order
|
|
||||||
|
|
||||||
- name: Get authzs for order
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
acme_version: 2
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ item }}"
|
|
||||||
method: get
|
|
||||||
loop: "{{ order.output_json.authorizations }}"
|
|
||||||
register: authz
|
|
||||||
- debug: var=authz
|
|
||||||
|
|
||||||
- name: Get HTTP-01 challenge for authz
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
acme_version: 2
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ (item.challenges | selectattr('type', 'equalto', 'http-01') | list)[0].url }}"
|
|
||||||
method: get
|
|
||||||
register: http01challenge
|
|
||||||
loop: "{{ authz.results | map(attribute='output_json') | list }}"
|
|
||||||
- debug: var=http01challenge
|
|
||||||
|
|
||||||
- name: Activate HTTP-01 challenge manually
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
acme_version: 2
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ item.url }}"
|
|
||||||
method: post
|
|
||||||
content: '{}'
|
|
||||||
register: activation
|
|
||||||
loop: "{{ http01challenge.results | map(attribute='output_json') | list }}"
|
|
||||||
- debug: var=activation
|
|
||||||
|
|
||||||
- name: Get HTTP-01 challenge results
|
|
||||||
acme_inspect:
|
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
|
||||||
acme_version: 2
|
|
||||||
validate_certs: no
|
|
||||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
|
||||||
account_uri: "{{ account_creation.headers.location }}"
|
|
||||||
url: "{{ item.url }}"
|
|
||||||
method: get
|
|
||||||
register: validation_result
|
|
||||||
loop: "{{ http01challenge.results | map(attribute='output_json') | list }}"
|
|
||||||
until: "validation_result.output_json.status != 'pending'"
|
|
||||||
retries: 20
|
|
||||||
delay: 1
|
|
||||||
- debug: var=validation_result
|
|
@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
- block:
|
|
||||||
- name: Running tests with OpenSSL backend
|
|
||||||
include_tasks: impl.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: openssl
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
|
||||||
|
|
||||||
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
|
|
||||||
when: openssl_version.stdout is version('1.0.0', '>=')
|
|
||||||
|
|
||||||
- name: Remove output directory
|
|
||||||
file:
|
|
||||||
path: "{{ output_dir }}"
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Re-create output directory
|
|
||||||
file:
|
|
||||||
path: "{{ output_dir }}"
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: Running tests with cryptography backend
|
|
||||||
include_tasks: impl.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: cryptography
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
|
||||||
|
|
||||||
when: cryptography_version.stdout is version('1.5', '>=')
|
|
@ -1,131 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Check directory output
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- directory is not changed
|
|
||||||
- "'directory' in directory"
|
|
||||||
- "'newAccount' in directory.directory"
|
|
||||||
- "'newOrder' in directory.directory"
|
|
||||||
- "'newNonce' in directory.directory"
|
|
||||||
- "'headers' not in directory"
|
|
||||||
- "'output_text' not in directory"
|
|
||||||
- "'output_json' not in directory"
|
|
||||||
|
|
||||||
- name: Check account creation output
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_creation is changed
|
|
||||||
- "'directory' in account_creation"
|
|
||||||
- "'headers' in account_creation"
|
|
||||||
- "'output_text' in account_creation"
|
|
||||||
- "'output_json' in account_creation"
|
|
||||||
- account_creation.headers.status == 201
|
|
||||||
- "'location' in account_creation.headers"
|
|
||||||
- account_creation.output_json.status == 'valid'
|
|
||||||
- not (account_creation.output_json.contact | default([]))
|
|
||||||
- account_creation.output_text | from_json == account_creation.output_json
|
|
||||||
|
|
||||||
- name: Check account get output
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_get is not changed
|
|
||||||
- "'directory' in account_get"
|
|
||||||
- "'headers' in account_get"
|
|
||||||
- "'output_text' in account_get"
|
|
||||||
- "'output_json' in account_get"
|
|
||||||
- account_get.headers.status == 200
|
|
||||||
- account_get.output_json == account_creation.output_json
|
|
||||||
|
|
||||||
- name: Check account update output
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- account_update is changed
|
|
||||||
- "'directory' in account_update"
|
|
||||||
- "'headers' in account_update"
|
|
||||||
- "'output_text' in account_update"
|
|
||||||
- "'output_json' in account_update"
|
|
||||||
- account_update.output_json.status == 'valid'
|
|
||||||
- account_update.output_json.contact | length == 1
|
|
||||||
- account_update.output_json.contact[0] == 'mailto:me@example.com'
|
|
||||||
|
|
||||||
- name: Check certificate request output
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- new_order is changed
|
|
||||||
- "'directory' in new_order"
|
|
||||||
- "'headers' in new_order"
|
|
||||||
- "'output_text' in new_order"
|
|
||||||
- "'output_json' in new_order"
|
|
||||||
- new_order.output_json.authorizations | length == 2
|
|
||||||
- new_order.output_json.identifiers | length == 2
|
|
||||||
- new_order.output_json.status == 'pending'
|
|
||||||
- "'finalize' in new_order.output_json"
|
|
||||||
|
|
||||||
- name: Check get order output
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- order is not changed
|
|
||||||
- "'directory' in order"
|
|
||||||
- "'headers' in order"
|
|
||||||
- "'output_text' in order"
|
|
||||||
- "'output_json' in order"
|
|
||||||
# The order of identifiers and authorizations is randomized!
|
|
||||||
# - new_order.output_json == order.output_json
|
|
||||||
|
|
||||||
- name: Check get authz output
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- item is not changed
|
|
||||||
- "'directory' in item"
|
|
||||||
- "'headers' in item"
|
|
||||||
- "'output_text' in item"
|
|
||||||
- "'output_json' in item"
|
|
||||||
- item.output_json.challenges | length >= 3
|
|
||||||
- item.output_json.identifier.type == 'dns'
|
|
||||||
- item.output_json.status == 'pending'
|
|
||||||
loop: "{{ authz.results }}"
|
|
||||||
|
|
||||||
- name: Check get challenge output
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- item is not changed
|
|
||||||
- "'directory' in item"
|
|
||||||
- "'headers' in item"
|
|
||||||
- "'output_text' in item"
|
|
||||||
- "'output_json' in item"
|
|
||||||
- item.output_json.status == 'pending'
|
|
||||||
- item.output_json.type == 'http-01'
|
|
||||||
- item.output_json.url == item.invocation.module_args.url
|
|
||||||
- "'token' in item.output_json"
|
|
||||||
loop: "{{ http01challenge.results }}"
|
|
||||||
|
|
||||||
- name: Check challenge activation output
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- item is changed
|
|
||||||
- "'directory' in item"
|
|
||||||
- "'headers' in item"
|
|
||||||
- "'output_text' in item"
|
|
||||||
- "'output_json' in item"
|
|
||||||
- item.output_json.status == 'pending'
|
|
||||||
- item.output_json.type == 'http-01'
|
|
||||||
- item.output_json.url == item.invocation.module_args.url
|
|
||||||
- "'token' in item.output_json"
|
|
||||||
loop: "{{ activation.results }}"
|
|
||||||
|
|
||||||
- name: Check validation result
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- item is not changed
|
|
||||||
- "'directory' in item"
|
|
||||||
- "'headers' in item"
|
|
||||||
- "'output_text' in item"
|
|
||||||
- "'output_json' in item"
|
|
||||||
- item.output_json.status == 'invalid'
|
|
||||||
- item.output_json.type == 'http-01'
|
|
||||||
- item.output_json.url == item.invocation.module_args.url
|
|
||||||
- "'token' in item.output_json"
|
|
||||||
- "'validated' in item.output_json"
|
|
||||||
- "'error' in item.output_json"
|
|
||||||
- item.output_json.error.type == 'urn:ietf:params:acme:error:unauthorized'
|
|
||||||
loop: "{{ validation_result.results }}"
|
|
@ -1,2 +0,0 @@
|
|||||||
shippable/posix/group1
|
|
||||||
skip/aix
|
|
@ -1,22 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDnzCCAyWgAwIBAgIQWyXOaQfEJlVm0zkMmalUrTAKBggqhkjOPQQDAzCBhTEL
|
|
||||||
MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
|
|
||||||
BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT
|
|
||||||
IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwOTI1MDAw
|
|
||||||
MDAwWhcNMjkwOTI0MjM1OTU5WjCBkjELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy
|
|
||||||
ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N
|
|
||||||
T0RPIENBIExpbWl0ZWQxODA2BgNVBAMTL0NPTU9ETyBFQ0MgRG9tYWluIFZhbGlk
|
|
||||||
YXRpb24gU2VjdXJlIFNlcnZlciBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
|
|
||||||
QgAEAjgZgTrJaYRwWQKOqIofMN+83gP8eR06JSxrQSEYgur5PkrkM8wSzypD/A7y
|
|
||||||
ZADA4SVQgiTNtkk4DyVHkUikraOCAWYwggFiMB8GA1UdIwQYMBaAFHVxpxlIGbyd
|
|
||||||
nepBR9+UxEh3mdN5MB0GA1UdDgQWBBRACWFn8LyDcU/eEggsb9TUK3Y9ljAOBgNV
|
|
||||||
HQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUEFjAUBggrBgEF
|
|
||||||
BQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgGBmeBDAECATBMBgNV
|
|
||||||
HR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9FQ0ND
|
|
||||||
ZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDByBggrBgEFBQcBAQRmMGQwOwYIKwYB
|
|
||||||
BQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9ET0VDQ0FkZFRydXN0
|
|
||||||
Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC5jb21vZG9jYTQuY29tMAoG
|
|
||||||
CCqGSM49BAMDA2gAMGUCMQCsaEclgBNPE1bAojcJl1pQxOfttGHLKIoKETKm4nHf
|
|
||||||
EQGJbwd6IGZrGNC5LkP3Um8CMBKFfI4TZpIEuppFCZRKMGHRSdxv6+ctyYnPHmp8
|
|
||||||
7IXOMCVZuoFwNLg0f+cB0eLLUg==
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,51 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFBTCCBKugAwIBAgIQL+c9oQXpvdcOD3BKAncbgDAKBggqhkjOPQQDAjCBkjEL
|
|
||||||
MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
|
|
||||||
BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxODA2BgNVBAMT
|
|
||||||
L0NPTU9ETyBFQ0MgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQSAy
|
|
||||||
MB4XDTE4MDcxMTAwMDAwMFoXDTE5MDExNzIzNTk1OVowbDEhMB8GA1UECxMYRG9t
|
|
||||||
YWluIENvbnRyb2wgVmFsaWRhdGVkMSEwHwYDVQQLExhQb3NpdGl2ZVNTTCBNdWx0
|
|
||||||
aS1Eb21haW4xJDAiBgNVBAMTG3NzbDgwMzAyNS5jbG91ZGZsYXJlc3NsLmNvbTBZ
|
|
||||||
MBMGByqGSM49AgEGCCqGSM49AwEHA0IABMap9sMZnCzTXID1chTOmtOk8p6+SHbG
|
|
||||||
3fmyJJljI7sN9RddlLKar9VBS48WguVv1R6trvERIYj8TzKCVBzu9mmjggMGMIID
|
|
||||||
AjAfBgNVHSMEGDAWgBRACWFn8LyDcU/eEggsb9TUK3Y9ljAdBgNVHQ4EFgQUd/6a
|
|
||||||
t8j7v5DsL7xWacf8VyzOLJcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAw
|
|
||||||
HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCME8GA1UdIARIMEYwOgYLKwYB
|
|
||||||
BAGyMQECAgcwKzApBggrBgEFBQcCARYdaHR0cHM6Ly9zZWN1cmUuY29tb2RvLmNv
|
|
||||||
bS9DUFMwCAYGZ4EMAQIBMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwuY29t
|
|
||||||
b2RvY2E0LmNvbS9DT01PRE9FQ0NEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVy
|
|
||||||
Q0EyLmNybDCBiAYIKwYBBQUHAQEEfDB6MFEGCCsGAQUFBzAChkVodHRwOi8vY3J0
|
|
||||||
LmNvbW9kb2NhNC5jb20vQ09NT0RPRUNDRG9tYWluVmFsaWRhdGlvblNlY3VyZVNl
|
|
||||||
cnZlckNBMi5jcnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLmNvbW9kb2NhNC5j
|
|
||||||
b20wSAYDVR0RBEEwP4Ibc3NsODAzMDI1LmNsb3VkZmxhcmVzc2wuY29tghAqLmhz
|
|
||||||
Y29zY2RuNDAubmV0gg5oc2Nvc2NkbjQwLm5ldDCCAQMGCisGAQQB1nkCBAIEgfQE
|
|
||||||
gfEA7wB2AO5Lvbd1zmC64UJpH6vhnmajD35fsHLYgwDEe4l6qP3LAAABZIbVA88A
|
|
||||||
AAQDAEcwRQIhANtN489Izy3iss/eF8rUw/gir8rqyA2t3lpxnco+J2NlAiBBku5M
|
|
||||||
iGD8whW5/31byPj0/ype1MmG0QYrq3qWvYiQ3QB1AHR+2oMxrTMQkSGcziVPQnDC
|
|
||||||
v/1eQiAIxjc1eeYQe8xWAAABZIbVBB4AAAQDAEYwRAIgSjcL7B4cbgm2XED69G7/
|
|
||||||
iFPe2zkWhxnkgGISSwuXw1gCICzwPmfbjEfwDNXEuBs7JXkPRaT1pi7hZ9aR5wJJ
|
|
||||||
TKH9MAoGCCqGSM49BAMCA0gAMEUCIQDqxmFLcme3Ldd+jiMQf7fT5pSezZfMOL0S
|
|
||||||
cNmfGvNtPQIgec3sO/ylnnaztCy5KDjYsnh+rm01bxs+nz2DnOPF+xo=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDnzCCAyWgAwIBAgIQWyXOaQfEJlVm0zkMmalUrTAKBggqhkjOPQQDAzCBhTEL
|
|
||||||
MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
|
|
||||||
BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT
|
|
||||||
IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwOTI1MDAw
|
|
||||||
MDAwWhcNMjkwOTI0MjM1OTU5WjCBkjELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy
|
|
||||||
ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N
|
|
||||||
T0RPIENBIExpbWl0ZWQxODA2BgNVBAMTL0NPTU9ETyBFQ0MgRG9tYWluIFZhbGlk
|
|
||||||
YXRpb24gU2VjdXJlIFNlcnZlciBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
|
|
||||||
QgAEAjgZgTrJaYRwWQKOqIofMN+83gP8eR06JSxrQSEYgur5PkrkM8wSzypD/A7y
|
|
||||||
ZADA4SVQgiTNtkk4DyVHkUikraOCAWYwggFiMB8GA1UdIwQYMBaAFHVxpxlIGbyd
|
|
||||||
nepBR9+UxEh3mdN5MB0GA1UdDgQWBBRACWFn8LyDcU/eEggsb9TUK3Y9ljAOBgNV
|
|
||||||
HQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUEFjAUBggrBgEF
|
|
||||||
BQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgGBmeBDAECATBMBgNV
|
|
||||||
HR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9FQ0ND
|
|
||||||
ZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDByBggrBgEFBQcBAQRmMGQwOwYIKwYB
|
|
||||||
BQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9ET0VDQ0FkZFRydXN0
|
|
||||||
Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC5jb21vZG9jYTQuY29tMAoG
|
|
||||||
CCqGSM49BAMDA2gAMGUCMQCsaEclgBNPE1bAojcJl1pQxOfttGHLKIoKETKm4nHf
|
|
||||||
EQGJbwd6IGZrGNC5LkP3Um8CMBKFfI4TZpIEuppFCZRKMGHRSdxv6+ctyYnPHmp8
|
|
||||||
7IXOMCVZuoFwNLg0f+cB0eLLUg==
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,16 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL
|
|
||||||
MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
|
|
||||||
BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT
|
|
||||||
IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw
|
|
||||||
MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy
|
|
||||||
ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N
|
|
||||||
T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv
|
|
||||||
biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR
|
|
||||||
FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J
|
|
||||||
cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW
|
|
||||||
BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/
|
|
||||||
BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm
|
|
||||||
fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv
|
|
||||||
GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,29 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFBTCCBKugAwIBAgIQL+c9oQXpvdcOD3BKAncbgDAKBggqhkjOPQQDAjCBkjEL
|
|
||||||
MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
|
|
||||||
BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxODA2BgNVBAMT
|
|
||||||
L0NPTU9ETyBFQ0MgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQSAy
|
|
||||||
MB4XDTE4MDcxMTAwMDAwMFoXDTE5MDExNzIzNTk1OVowbDEhMB8GA1UECxMYRG9t
|
|
||||||
YWluIENvbnRyb2wgVmFsaWRhdGVkMSEwHwYDVQQLExhQb3NpdGl2ZVNTTCBNdWx0
|
|
||||||
aS1Eb21haW4xJDAiBgNVBAMTG3NzbDgwMzAyNS5jbG91ZGZsYXJlc3NsLmNvbTBZ
|
|
||||||
MBMGByqGSM49AgEGCCqGSM49AwEHA0IABMap9sMZnCzTXID1chTOmtOk8p6+SHbG
|
|
||||||
3fmyJJljI7sN9RddlLKar9VBS48WguVv1R6trvERIYj8TzKCVBzu9mmjggMGMIID
|
|
||||||
AjAfBgNVHSMEGDAWgBRACWFn8LyDcU/eEggsb9TUK3Y9ljAdBgNVHQ4EFgQUd/6a
|
|
||||||
t8j7v5DsL7xWacf8VyzOLJcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAw
|
|
||||||
HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCME8GA1UdIARIMEYwOgYLKwYB
|
|
||||||
BAGyMQECAgcwKzApBggrBgEFBQcCARYdaHR0cHM6Ly9zZWN1cmUuY29tb2RvLmNv
|
|
||||||
bS9DUFMwCAYGZ4EMAQIBMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwuY29t
|
|
||||||
b2RvY2E0LmNvbS9DT01PRE9FQ0NEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVy
|
|
||||||
Q0EyLmNybDCBiAYIKwYBBQUHAQEEfDB6MFEGCCsGAQUFBzAChkVodHRwOi8vY3J0
|
|
||||||
LmNvbW9kb2NhNC5jb20vQ09NT0RPRUNDRG9tYWluVmFsaWRhdGlvblNlY3VyZVNl
|
|
||||||
cnZlckNBMi5jcnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLmNvbW9kb2NhNC5j
|
|
||||||
b20wSAYDVR0RBEEwP4Ibc3NsODAzMDI1LmNsb3VkZmxhcmVzc2wuY29tghAqLmhz
|
|
||||||
Y29zY2RuNDAubmV0gg5oc2Nvc2NkbjQwLm5ldDCCAQMGCisGAQQB1nkCBAIEgfQE
|
|
||||||
gfEA7wB2AO5Lvbd1zmC64UJpH6vhnmajD35fsHLYgwDEe4l6qP3LAAABZIbVA88A
|
|
||||||
AAQDAEcwRQIhANtN489Izy3iss/eF8rUw/gir8rqyA2t3lpxnco+J2NlAiBBku5M
|
|
||||||
iGD8whW5/31byPj0/ype1MmG0QYrq3qWvYiQ3QB1AHR+2oMxrTMQkSGcziVPQnDC
|
|
||||||
v/1eQiAIxjc1eeYQe8xWAAABZIbVBB4AAAQDAEYwRAIgSjcL7B4cbgm2XED69G7/
|
|
||||||
iFPe2zkWhxnkgGISSwuXw1gCICzwPmfbjEfwDNXEuBs7JXkPRaT1pi7hZ9aR5wJJ
|
|
||||||
TKH9MAoGCCqGSM49BAMCA0gAMEUCIQDqxmFLcme3Ldd+jiMQf7fT5pSezZfMOL0S
|
|
||||||
cNmfGvNtPQIgec3sO/ylnnaztCy5KDjYsnh+rm01bxs+nz2DnOPF+xo=
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,32 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFjTCCA3WgAwIBAgIRANOxciY0IzLc9AUoUSrsnGowDQYJKoZIhvcNAQELBQAw
|
|
||||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
|
||||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTYxMDA2MTU0MzU1
|
|
||||||
WhcNMjExMDA2MTU0MzU1WjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
|
||||||
RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDMwggEi
|
|
||||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc0wzwWuUuR7dyXTeDs2hjMOrX
|
|
||||||
NSYZJeG9vjXxcJIvt7hLQQWrqZ41CFjssSrEaIcLo+N15Obzp2JxunmBYB/XkZqf
|
|
||||||
89B4Z3HIaQ6Vkc/+5pnpYDxIzH7KTXcSJJ1HG1rrueweNwAcnKx7pwXqzkrrvUHl
|
|
||||||
Npi5y/1tPJZo3yMqQpAMhnRnyH+lmrhSYRQTP2XpgofL2/oOVvaGifOFP5eGr7Dc
|
|
||||||
Gu9rDZUWfcQroGWymQQ2dYBrrErzG5BJeC+ilk8qICUpBMZ0wNAxzY8xOJUWuqgz
|
|
||||||
uEPxsR/DMH+ieTETPS02+OP88jNquTkxxa/EjQ0dZBYzqvqEKbbUC8DYfcOTAgMB
|
|
||||||
AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU
|
|
||||||
BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB
|
|
||||||
FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBSo
|
|
||||||
SmpjBH3duubRObemRWXv86jsoTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js
|
|
||||||
LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF
|
|
||||||
BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG
|
|
||||||
AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD
|
|
||||||
VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB
|
|
||||||
ABnPdSA0LTqmRf/Q1eaM2jLonG4bQdEnqOJQ8nCqxOeTRrToEKtwT++36gTSlBGx
|
|
||||||
A/5dut82jJQ2jxN8RI8L9QFXrWi4xXnA2EqA10yjHiR6H9cj6MFiOnb5In1eWsRM
|
|
||||||
UM2v3e9tNsCAgBukPHAg1lQh07rvFKm/Bz9BCjaxorALINUfZ9DD64j2igLIxle2
|
|
||||||
DPxW8dI/F2loHMjXZjqG8RkqZUdoxtID5+90FgsGIfkMpqgRS05f4zPbCEHqCXl1
|
|
||||||
eO5HyELTgcVlLXXQDgAWnRzut1hFJeczY1tjQQno6f6s+nMydLN26WuU4s3UYvOu
|
|
||||||
OsUxRlJu7TSRHqDC3lSE5XggVkzdaPkuKGQbGpny+01/47hfXXNB7HntWNZ6N2Vw
|
|
||||||
p7G6OfY+YQrZwIaQmhrIqJZuigsrbe3W+gdn5ykE9+Ky0VgVUsfxo52mwFYs1JKY
|
|
||||||
2PGDuWx8M6DlS6qQkvHaRUo0FMd8TsSlbF0/v965qGFKhSDeQoMpYnwcmQilRh/0
|
|
||||||
ayLThlHLN81gSkJjVrPI0Y8xCVPB4twb1PFUd2fPM3sA1tJ83sZ5v8vgFv2yofKR
|
|
||||||
PB0t6JzUA81mSqM3kxl5e+IZwhYAyO0OTg3/fs8HqGTNKd9BqoUwSRBzp06JMg5b
|
|
||||||
rUCGwbCUDI0mxadJ3Bz4WxR6fyNpBK2yAinWEsikxqEt
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,31 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
|
||||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
|
||||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
|
||||||
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
|
||||||
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
|
||||||
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
|
||||||
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
|
||||||
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
|
||||||
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
|
||||||
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
|
||||||
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
|
||||||
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
|
||||||
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
|
||||||
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
|
||||||
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
|
||||||
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
|
||||||
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
|
||||||
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
|
||||||
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
|
||||||
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
|
||||||
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
|
||||||
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
|
||||||
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
|
||||||
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
|
||||||
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
|
||||||
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
|
||||||
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
|
||||||
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
|
||||||
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,27 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
|
|
||||||
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
|
|
||||||
DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
|
|
||||||
SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
|
|
||||||
GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
|
|
||||||
AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
|
|
||||||
q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
|
|
||||||
SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
|
|
||||||
Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
|
|
||||||
a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
|
|
||||||
/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
|
|
||||||
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
|
|
||||||
CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
|
|
||||||
bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
|
|
||||||
c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
|
|
||||||
VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
|
|
||||||
ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
|
|
||||||
MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
|
|
||||||
Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
|
|
||||||
AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
|
|
||||||
uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
|
|
||||||
wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
|
|
||||||
X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
|
|
||||||
PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
|
|
||||||
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,72 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIH5jCCBs6gAwIBAgISA2gSCm/BtvCR2e2bIap5YbXaMA0GCSqGSIb3DQEBCwUA
|
|
||||||
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
|
|
||||||
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xODA3MjcxNzMxMjdaFw0x
|
|
||||||
ODEwMjUxNzMxMjdaMB4xHDAaBgNVBAMTE3d3dy5sZXRzZW5jcnlwdC5vcmcwggEi
|
|
||||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDpL8ZjVL0MUkUAIbYO9+ZCni+c
|
|
||||||
ghGd9WhM2Ztaay6Wyh6lNoCdltdqTwUhE4O+d7UFModjM3G/KMyfuujr06c5iGKL
|
|
||||||
3saPmIzLaRPIEOUlB2rKgasKhe8mDRyRLzQSXXgnsaKcTBBuhIHvtP51ZMr05nJJ
|
|
||||||
sX/5FGjj96w+KJel6E/Ux1a1ZDOFkAYNSIrJJhA5jjIvUPr+Ri6Oc6UlhF9oueKI
|
|
||||||
uWBILxQpC778tBWdHoZeBCNTHA1VvtwC53OeuHvdZm1jB/e30Mgf5DtVizYpFXVD
|
|
||||||
mztkrd6z/3B6ZwPyfCE4KgzSf70/byOz971OJxNKTUVWedKHHDlrMxfsPclbAgMB
|
|
||||||
AAGjggTwMIIE7DAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
|
|
||||||
CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFG1w4j/KDrYSFu7m9DPE
|
|
||||||
xRR0E5gzMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUF
|
|
||||||
BwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNy
|
|
||||||
eXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNy
|
|
||||||
eXB0Lm9yZy8wggHxBgNVHREEggHoMIIB5IIbY2VydC5pbnQteDEubGV0c2VuY3J5
|
|
||||||
cHQub3JnghtjZXJ0LmludC14Mi5sZXRzZW5jcnlwdC5vcmeCG2NlcnQuaW50LXgz
|
|
||||||
LmxldHNlbmNyeXB0Lm9yZ4IbY2VydC5pbnQteDQubGV0c2VuY3J5cHQub3Jnghxj
|
|
||||||
ZXJ0LnJvb3QteDEubGV0c2VuY3J5cHQub3Jngh9jZXJ0LnN0YWdpbmcteDEubGV0
|
|
||||||
c2VuY3J5cHQub3Jngh9jZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JngiBj
|
|
||||||
ZXJ0LnN0Zy1yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ISY3AubGV0c2VuY3J5cHQu
|
|
||||||
b3JnghpjcC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ITY3BzLmxldHNlbmNyeXB0
|
|
||||||
Lm9yZ4IbY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3Jnghtjcmwucm9vdC14MS5s
|
|
||||||
ZXRzZW5jcnlwdC5vcmeCD2xldHNlbmNyeXB0Lm9yZ4IWb3JpZ2luLmxldHNlbmNy
|
|
||||||
eXB0Lm9yZ4IXb3JpZ2luMi5sZXRzZW5jcnlwdC5vcmeCFnN0YXR1cy5sZXRzZW5j
|
|
||||||
cnlwdC5vcmeCE3d3dy5sZXRzZW5jcnlwdC5vcmcwgf4GA1UdIASB9jCB8zAIBgZn
|
|
||||||
gQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3Bz
|
|
||||||
LmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmlj
|
|
||||||
YXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBh
|
|
||||||
bmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGlj
|
|
||||||
eSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzCC
|
|
||||||
AQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AMEWSuCnctLUOS3ICsEHcNTwxJvemRpI
|
|
||||||
QMH6B1Fk9jNgAAABZN0ChToAAAQDAEcwRQIgblal8oXnfoopr1+dWVhvBx+sqHT0
|
|
||||||
eLYxJHBTaRp3j1QCIQDhFQqMk6DDXUgcU12K36zLVFwJTdAJI4RBisnX+g+W0AB2
|
|
||||||
ACk8UZZUyDlluqpQ/FgH1Ldvv1h6KXLcpMMM9OVFR/R4AAABZN0Chz4AAAQDAEcw
|
|
||||||
RQIhAImOjvkritUNKJZB7dcUtjoyIbfNwdCspvRiEzXuvVQoAiAZryoyg3TcMun5
|
|
||||||
Gb2dEn1cttMnPW9u670/JdRjvjU/wTANBgkqhkiG9w0BAQsFAAOCAQEAGepCmckP
|
|
||||||
Tn9Sz268FEwkdD+6wWaPfeYlh+9nacFh90nQ35EYQMOK8a+X7ixHGbRz19On3Wt4
|
|
||||||
1fcbPa9SefocTjAintMwwreCxpRTmwGACYojd7vRWEmA6q7+/HO2BfZahWzclOjw
|
|
||||||
mSDBycDEm8R0ZK52vYjzVno8x0mrsmSO0403S/6syYB/guH6P17kIBw+Tgx6/i/c
|
|
||||||
I1C6MoFkuaAKUUcZmgGGBgE+L/7cWtWjbkVXyA3ZQQy9G7rcBT+N/RrDfBh4iZDq
|
|
||||||
jAN5UIIYL8upBhjiMYVuoJrH2nklzEwr5SWKcccJX5eWkGLUwlcY9LGAA8+17l2I
|
|
||||||
l1Ou20Dm9TxnNw==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
|
|
||||||
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
|
|
||||||
DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
|
|
||||||
SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
|
|
||||||
GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
|
|
||||||
AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
|
|
||||||
q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
|
|
||||||
SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
|
|
||||||
Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
|
|
||||||
a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
|
|
||||||
/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
|
|
||||||
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
|
|
||||||
CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
|
|
||||||
bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
|
|
||||||
c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
|
|
||||||
VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
|
|
||||||
ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
|
|
||||||
MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
|
|
||||||
Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
|
|
||||||
AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
|
|
||||||
uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
|
|
||||||
wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
|
|
||||||
X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
|
|
||||||
PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
|
|
||||||
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,20 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
|
|
||||||
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
|
|
||||||
DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
|
|
||||||
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
|
|
||||||
Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
|
||||||
AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
|
|
||||||
rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
|
|
||||||
OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
|
|
||||||
xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
|
|
||||||
7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
|
|
||||||
aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
|
|
||||||
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
|
|
||||||
SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
|
|
||||||
ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
|
|
||||||
AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
|
|
||||||
R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
|
|
||||||
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
|
|
||||||
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,45 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIH5jCCBs6gAwIBAgISA2gSCm/BtvCR2e2bIap5YbXaMA0GCSqGSIb3DQEBCwUA
|
|
||||||
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
|
|
||||||
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xODA3MjcxNzMxMjdaFw0x
|
|
||||||
ODEwMjUxNzMxMjdaMB4xHDAaBgNVBAMTE3d3dy5sZXRzZW5jcnlwdC5vcmcwggEi
|
|
||||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDpL8ZjVL0MUkUAIbYO9+ZCni+c
|
|
||||||
ghGd9WhM2Ztaay6Wyh6lNoCdltdqTwUhE4O+d7UFModjM3G/KMyfuujr06c5iGKL
|
|
||||||
3saPmIzLaRPIEOUlB2rKgasKhe8mDRyRLzQSXXgnsaKcTBBuhIHvtP51ZMr05nJJ
|
|
||||||
sX/5FGjj96w+KJel6E/Ux1a1ZDOFkAYNSIrJJhA5jjIvUPr+Ri6Oc6UlhF9oueKI
|
|
||||||
uWBILxQpC778tBWdHoZeBCNTHA1VvtwC53OeuHvdZm1jB/e30Mgf5DtVizYpFXVD
|
|
||||||
mztkrd6z/3B6ZwPyfCE4KgzSf70/byOz971OJxNKTUVWedKHHDlrMxfsPclbAgMB
|
|
||||||
AAGjggTwMIIE7DAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
|
|
||||||
CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFG1w4j/KDrYSFu7m9DPE
|
|
||||||
xRR0E5gzMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUF
|
|
||||||
BwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNy
|
|
||||||
eXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNy
|
|
||||||
eXB0Lm9yZy8wggHxBgNVHREEggHoMIIB5IIbY2VydC5pbnQteDEubGV0c2VuY3J5
|
|
||||||
cHQub3JnghtjZXJ0LmludC14Mi5sZXRzZW5jcnlwdC5vcmeCG2NlcnQuaW50LXgz
|
|
||||||
LmxldHNlbmNyeXB0Lm9yZ4IbY2VydC5pbnQteDQubGV0c2VuY3J5cHQub3Jnghxj
|
|
||||||
ZXJ0LnJvb3QteDEubGV0c2VuY3J5cHQub3Jngh9jZXJ0LnN0YWdpbmcteDEubGV0
|
|
||||||
c2VuY3J5cHQub3Jngh9jZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JngiBj
|
|
||||||
ZXJ0LnN0Zy1yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ISY3AubGV0c2VuY3J5cHQu
|
|
||||||
b3JnghpjcC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ITY3BzLmxldHNlbmNyeXB0
|
|
||||||
Lm9yZ4IbY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3Jnghtjcmwucm9vdC14MS5s
|
|
||||||
ZXRzZW5jcnlwdC5vcmeCD2xldHNlbmNyeXB0Lm9yZ4IWb3JpZ2luLmxldHNlbmNy
|
|
||||||
eXB0Lm9yZ4IXb3JpZ2luMi5sZXRzZW5jcnlwdC5vcmeCFnN0YXR1cy5sZXRzZW5j
|
|
||||||
cnlwdC5vcmeCE3d3dy5sZXRzZW5jcnlwdC5vcmcwgf4GA1UdIASB9jCB8zAIBgZn
|
|
||||||
gQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3Bz
|
|
||||||
LmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmlj
|
|
||||||
YXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBh
|
|
||||||
bmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGlj
|
|
||||||
eSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzCC
|
|
||||||
AQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AMEWSuCnctLUOS3ICsEHcNTwxJvemRpI
|
|
||||||
QMH6B1Fk9jNgAAABZN0ChToAAAQDAEcwRQIgblal8oXnfoopr1+dWVhvBx+sqHT0
|
|
||||||
eLYxJHBTaRp3j1QCIQDhFQqMk6DDXUgcU12K36zLVFwJTdAJI4RBisnX+g+W0AB2
|
|
||||||
ACk8UZZUyDlluqpQ/FgH1Ldvv1h6KXLcpMMM9OVFR/R4AAABZN0Chz4AAAQDAEcw
|
|
||||||
RQIhAImOjvkritUNKJZB7dcUtjoyIbfNwdCspvRiEzXuvVQoAiAZryoyg3TcMun5
|
|
||||||
Gb2dEn1cttMnPW9u670/JdRjvjU/wTANBgkqhkiG9w0BAQsFAAOCAQEAGepCmckP
|
|
||||||
Tn9Sz268FEwkdD+6wWaPfeYlh+9nacFh90nQ35EYQMOK8a+X7ixHGbRz19On3Wt4
|
|
||||||
1fcbPa9SefocTjAintMwwreCxpRTmwGACYojd7vRWEmA6q7+/HO2BfZahWzclOjw
|
|
||||||
mSDBycDEm8R0ZK52vYjzVno8x0mrsmSO0403S/6syYB/guH6P17kIBw+Tgx6/i/c
|
|
||||||
I1C6MoFkuaAKUUcZmgGGBgE+L/7cWtWjbkVXyA3ZQQy9G7rcBT+N/RrDfBh4iZDq
|
|
||||||
jAN5UIIYL8upBhjiMYVuoJrH2nklzEwr5SWKcccJX5eWkGLUwlcY9LGAA8+17l2I
|
|
||||||
l1Ou20Dm9TxnNw==
|
|
||||||
-----END CERTIFICATE-----
|
|
File diff suppressed because it is too large
Load Diff
@ -1,25 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB
|
|
||||||
gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
|
||||||
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV
|
|
||||||
BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw
|
|
||||||
MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl
|
|
||||||
YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P
|
|
||||||
RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0
|
|
||||||
aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3
|
|
||||||
UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI
|
|
||||||
2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8
|
|
||||||
Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp
|
|
||||||
+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+
|
|
||||||
DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O
|
|
||||||
nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW
|
|
||||||
/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g
|
|
||||||
PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u
|
|
||||||
QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY
|
|
||||||
SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv
|
|
||||||
IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/
|
|
||||||
RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4
|
|
||||||
zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd
|
|
||||||
BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB
|
|
||||||
ZQ==
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,16 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL
|
|
||||||
MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
|
|
||||||
BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT
|
|
||||||
IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw
|
|
||||||
MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy
|
|
||||||
ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N
|
|
||||||
T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv
|
|
||||||
biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR
|
|
||||||
FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J
|
|
||||||
cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW
|
|
||||||
BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/
|
|
||||||
BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm
|
|
||||||
fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv
|
|
||||||
GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,34 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB
|
|
||||||
hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
|
||||||
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
|
|
||||||
BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5
|
|
||||||
MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
|
|
||||||
EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
|
|
||||||
Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh
|
|
||||||
dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR
|
|
||||||
6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X
|
|
||||||
pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC
|
|
||||||
9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV
|
|
||||||
/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf
|
|
||||||
Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z
|
|
||||||
+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w
|
|
||||||
qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah
|
|
||||||
SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC
|
|
||||||
u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf
|
|
||||||
Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq
|
|
||||||
crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E
|
|
||||||
FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB
|
|
||||||
/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl
|
|
||||||
wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM
|
|
||||||
4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV
|
|
||||||
2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna
|
|
||||||
FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ
|
|
||||||
CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK
|
|
||||||
boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke
|
|
||||||
jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL
|
|
||||||
S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb
|
|
||||||
QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl
|
|
||||||
0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB
|
|
||||||
NVOFBkpdn627G190
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,20 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
|
|
||||||
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
|
|
||||||
DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
|
|
||||||
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
|
|
||||||
Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
|
||||||
AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
|
|
||||||
rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
|
|
||||||
OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
|
|
||||||
xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
|
|
||||||
7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
|
|
||||||
aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
|
|
||||||
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
|
|
||||||
SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
|
|
||||||
ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
|
|
||||||
AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
|
|
||||||
R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
|
|
||||||
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
|
|
||||||
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,31 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
|
||||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
|
||||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
|
||||||
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
|
||||||
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
|
||||||
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
|
||||||
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
|
||||||
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
|
||||||
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
|
||||||
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
|
||||||
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
|
||||||
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
|
||||||
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
|
||||||
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
|
||||||
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
|
||||||
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
|
||||||
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
|
||||||
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
|
||||||
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
|
||||||
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
|
||||||
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
|
||||||
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
|
||||||
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
|
||||||
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
|
||||||
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
|
||||||
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
|
||||||
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
|
||||||
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
|
||||||
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,2 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- setup_remote_tmp_dir
|
|
@ -1,97 +0,0 @@
|
|||||||
---
|
|
||||||
- name: register cryptography version
|
|
||||||
command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'"
|
|
||||||
register: cryptography_version
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: Archive test files
|
|
||||||
archive:
|
|
||||||
path: "{{ role_path }}/files/"
|
|
||||||
dest: "{{ output_dir }}/files.tgz"
|
|
||||||
|
|
||||||
- name: Create temporary directory to store files
|
|
||||||
file:
|
|
||||||
state: directory
|
|
||||||
path: "{{ remote_tmp_dir }}/files/"
|
|
||||||
|
|
||||||
- name: Unarchive test files on testhost
|
|
||||||
unarchive:
|
|
||||||
src: "{{ output_dir }}/files.tgz"
|
|
||||||
dest: "{{ remote_tmp_dir }}/files/"
|
|
||||||
|
|
||||||
# Cert 1: certificate for www.ansible.com, retrieved on 2018-08-15
|
|
||||||
- name: Find root for cert 1
|
|
||||||
certificate_complete_chain:
|
|
||||||
input_chain: "{{ lookup('file', 'cert1-fullchain.pem', rstrip=False) }}"
|
|
||||||
root_certificates:
|
|
||||||
- "{{ remote_tmp_dir }}/files/roots/"
|
|
||||||
register: cert1_root
|
|
||||||
- name: Verify root for cert 1
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "cert1_root.complete_chain | join('') == (lookup('file', 'cert1.pem', rstrip=False) ~ lookup('file', 'cert1-chain.pem', rstrip=False) ~ lookup('file', 'cert1-root.pem', rstrip=False))"
|
|
||||||
- "cert1_root.root == lookup('file', 'cert1-root.pem', rstrip=False)"
|
|
||||||
|
|
||||||
- name: Find rootchain for cert 1
|
|
||||||
certificate_complete_chain:
|
|
||||||
input_chain: "{{ lookup('file', 'cert1.pem', rstrip=False) }}"
|
|
||||||
intermediate_certificates:
|
|
||||||
- "{{ remote_tmp_dir }}/files/cert1-chain.pem"
|
|
||||||
root_certificates:
|
|
||||||
- "{{ remote_tmp_dir }}/files/roots.pem"
|
|
||||||
register: cert1_rootchain
|
|
||||||
- name: Verify rootchain for cert 1
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "cert1_rootchain.complete_chain | join('') == (lookup('file', 'cert1.pem', rstrip=False) ~ lookup('file', 'cert1-chain.pem', rstrip=False) ~ lookup('file', 'cert1-root.pem', rstrip=False))"
|
|
||||||
- "cert1_rootchain.chain[:-1] | join('') == lookup('file', 'cert1-chain.pem', rstrip=False)"
|
|
||||||
- "cert1_rootchain.root == lookup('file', 'cert1-root.pem', rstrip=False)"
|
|
||||||
|
|
||||||
# Cert 2: certificate for letsencrypt.org, retrieved on 2018-08-15
|
|
||||||
# Intermediate: cross-signed by IdenTrust
|
|
||||||
- name: Find root for cert 2
|
|
||||||
certificate_complete_chain:
|
|
||||||
input_chain: "{{ lookup('file', 'cert2-fullchain.pem', rstrip=False) }}"
|
|
||||||
root_certificates:
|
|
||||||
- "{{ remote_tmp_dir }}/files/roots/"
|
|
||||||
register: cert2_root
|
|
||||||
- name: Verify root for cert 2
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "cert2_root.complete_chain | join('') == (lookup('file', 'cert2.pem', rstrip=False) ~ lookup('file', 'cert2-chain.pem', rstrip=False) ~ lookup('file', 'cert2-root.pem', rstrip=False))"
|
|
||||||
- "cert2_root.root == lookup('file', 'cert2-root.pem', rstrip=False)"
|
|
||||||
|
|
||||||
- name: Find rootchain for cert 2
|
|
||||||
certificate_complete_chain:
|
|
||||||
input_chain: "{{ lookup('file', 'cert2.pem', rstrip=False) }}"
|
|
||||||
intermediate_certificates:
|
|
||||||
- "{{ remote_tmp_dir }}/files/cert2-chain.pem"
|
|
||||||
root_certificates:
|
|
||||||
- "{{ remote_tmp_dir }}/files/roots.pem"
|
|
||||||
register: cert2_rootchain
|
|
||||||
- name: Verify rootchain for cert 2
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "cert2_rootchain.complete_chain | join('') == (lookup('file', 'cert2.pem', rstrip=False) ~ lookup('file', 'cert2-chain.pem', rstrip=False) ~ lookup('file', 'cert2-root.pem', rstrip=False))"
|
|
||||||
- "cert2_rootchain.chain[:-1] | join('') == lookup('file', 'cert2-chain.pem', rstrip=False)"
|
|
||||||
- "cert2_rootchain.root == lookup('file', 'cert2-root.pem', rstrip=False)"
|
|
||||||
|
|
||||||
# Cert 2: certificate for letsencrypt.org, retrieved on 2018-08-15
|
|
||||||
# Intermediate: signed by ISRG root
|
|
||||||
- name: Find alternate rootchain for cert 2
|
|
||||||
certificate_complete_chain:
|
|
||||||
# Remove line ending, make sure it is re-added by code
|
|
||||||
input_chain: "{{ lookup('file', 'cert2.pem', rstrip=True) }}"
|
|
||||||
intermediate_certificates:
|
|
||||||
- "{{ remote_tmp_dir }}/files/cert2-altchain.pem"
|
|
||||||
root_certificates:
|
|
||||||
- "{{ remote_tmp_dir }}/files/roots.pem"
|
|
||||||
register: cert2_rootchain_alt
|
|
||||||
- name: Verify rootchain for cert 2
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "cert2_rootchain_alt.complete_chain | join('') == (lookup('file', 'cert2.pem', rstrip=False) ~ lookup('file', 'cert2-altchain.pem', rstrip=False) ~ lookup('file', 'cert2-altroot.pem', rstrip=False))"
|
|
||||||
- "cert2_rootchain_alt.chain[:-1] | join('') == lookup('file', 'cert2-altchain.pem', rstrip=False)"
|
|
||||||
- "cert2_rootchain_alt.root == lookup('file', 'cert2-altroot.pem', rstrip=False)"
|
|
||||||
|
|
||||||
when: cryptography_version.stdout is version('1.5', '>=')
|
|
@ -1,15 +0,0 @@
|
|||||||
# Not enabled due to lack of access to test environments. May be enabled using custom integration_config.yml
|
|
||||||
# Example integation_config.yml
|
|
||||||
# ---
|
|
||||||
# entrust_api_user:
|
|
||||||
# entrust_api_key:
|
|
||||||
# entrust_api_client_cert_path: /var/integration-testing/publicCert.pem
|
|
||||||
# entrust_api_client_cert_key_path: /var/integration-testing/privateKey.pem
|
|
||||||
# entrust_api_ip_address: 127.0.0.1
|
|
||||||
# entrust_cloud_ip_address: 127.0.0.1
|
|
||||||
# # Used for certificate path validation of QA environments - we chose not to support disabling path validation ever.
|
|
||||||
# cacerts_bundle_path_local: /var/integration-testing/cacerts
|
|
||||||
|
|
||||||
### WARNING: This test will update HOSTS file and CERTIFICATE STORE of target host, in order to be able to validate
|
|
||||||
# to a QA environment. ###
|
|
||||||
unsupported
|
|
@ -1,2 +0,0 @@
|
|||||||
---
|
|
||||||
# defaults file for test_ecs_certificate
|
|
@ -1,3 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- prepare_tests
|
|
||||||
- setup_openssl
|
|
@ -1,215 +0,0 @@
|
|||||||
---
|
|
||||||
## Verify that integration_config was specified
|
|
||||||
- block:
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- entrust_api_user is defined
|
|
||||||
- entrust_api_key is defined
|
|
||||||
- entrust_api_ip_address is defined
|
|
||||||
- entrust_cloud_ip_address is defined
|
|
||||||
- entrust_api_client_cert_path is defined or entrust_api_client_cert_contents is defined
|
|
||||||
- entrust_api_client_cert_key_path is defined or entrust_api_client_cert_key_contents
|
|
||||||
- cacerts_bundle_path_local is defined
|
|
||||||
|
|
||||||
## SET UP TEST ENVIRONMENT ########################################################################
|
|
||||||
- name: copy the files needed for verifying test server certificate to the host
|
|
||||||
copy:
|
|
||||||
src: '{{ cacerts_bundle_path_local }}/'
|
|
||||||
dest: '{{ cacerts_bundle_path }}'
|
|
||||||
|
|
||||||
- name: Update the CA certificates for our QA certs (collection may need updating if new QA environments used)
|
|
||||||
command: c_rehash {{ cacerts_bundle_path }}
|
|
||||||
|
|
||||||
- name: Update hosts file
|
|
||||||
lineinfile:
|
|
||||||
path: /etc/hosts
|
|
||||||
state: present
|
|
||||||
regexp: 'api.entrust.net$'
|
|
||||||
line: '{{ entrust_api_ip_address }} api.entrust.net'
|
|
||||||
|
|
||||||
- name: Update hosts file
|
|
||||||
lineinfile:
|
|
||||||
path: /etc/hosts
|
|
||||||
state: present
|
|
||||||
regexp: 'cloud.entrust.net$'
|
|
||||||
line: '{{ entrust_cloud_ip_address }} cloud.entrust.net'
|
|
||||||
|
|
||||||
- name: Clear out the temporary directory for storing the API connection information
|
|
||||||
file:
|
|
||||||
path: '{{ tmpdir_path }}'
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Create a directory for storing the API connection Information
|
|
||||||
file:
|
|
||||||
path: '{{ tmpdir_path }}'
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- name: Copy the files needed for the connection to entrust API to the host
|
|
||||||
copy:
|
|
||||||
src: '{{ entrust_api_client_cert_path }}'
|
|
||||||
dest: '{{ entrust_api_cert }}'
|
|
||||||
|
|
||||||
- name: Copy the files needed for the connection to entrust API to the host
|
|
||||||
copy:
|
|
||||||
src: '{{ entrust_api_client_cert_key_path }}'
|
|
||||||
dest: '{{ entrust_api_cert_key }}'
|
|
||||||
|
|
||||||
## SETUP CSR TO REQUEST
|
|
||||||
- name: Generate a 2048 bit RSA private key
|
|
||||||
openssl_privatekey:
|
|
||||||
path: '{{ privatekey_path }}'
|
|
||||||
passphrase: '{{ privatekey_passphrase }}'
|
|
||||||
cipher: auto
|
|
||||||
type: RSA
|
|
||||||
size: 2048
|
|
||||||
|
|
||||||
- name: Generate a certificate signing request using the generated key
|
|
||||||
openssl_csr:
|
|
||||||
path: '{{ csr_path }}'
|
|
||||||
privatekey_path: '{{ privatekey_path }}'
|
|
||||||
privatekey_passphrase: '{{ privatekey_passphrase }}'
|
|
||||||
common_name: '{{ common_name }}'
|
|
||||||
organization_name: '{{ organization_name | default(omit) }}'
|
|
||||||
organizational_unit_name: '{{ organizational_unit_name | default(omit) }}'
|
|
||||||
country_name: '{{ country_name | default(omit) }}'
|
|
||||||
state_or_province_name: '{{ state_or_province_name | default(omit) }}'
|
|
||||||
digest: sha256
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: Have ECS generate a signed certificate
|
|
||||||
ecs_certificate:
|
|
||||||
backup: True
|
|
||||||
path: '{{ example1_cert_path }}'
|
|
||||||
full_chain_path: '{{ example1_chain_path }}'
|
|
||||||
csr: '{{ csr_path }}'
|
|
||||||
cert_type: '{{ example1_cert_type }}'
|
|
||||||
requester_name: '{{ entrust_requester_name }}'
|
|
||||||
requester_email: '{{ entrust_requester_email }}'
|
|
||||||
requester_phone: '{{ entrust_requester_phone }}'
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: example1_result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- example1_result is not failed
|
|
||||||
- example1_result.changed
|
|
||||||
- example1_result.tracking_id > 0
|
|
||||||
- example1_result.serial_number is string
|
|
||||||
|
|
||||||
# Internal CA refuses to issue certificates with the same DN in a short time frame
|
|
||||||
- name: Sleep for 5 seconds so we don't run into duplicate-request errors
|
|
||||||
pause:
|
|
||||||
seconds: 5
|
|
||||||
|
|
||||||
- name: Attempt to have ECS generate a signed certificate, but existing one is valid
|
|
||||||
ecs_certificate:
|
|
||||||
backup: True
|
|
||||||
path: '{{ example1_cert_path }}'
|
|
||||||
full_chain_path: '{{ example1_chain_path }}'
|
|
||||||
csr: '{{ csr_path }}'
|
|
||||||
cert_type: '{{ example1_cert_type }}'
|
|
||||||
requester_name: '{{ entrust_requester_name }}'
|
|
||||||
requester_email: '{{ entrust_requester_email }}'
|
|
||||||
requester_phone: '{{ entrust_requester_phone }}'
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: example2_result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- example2_result is not failed
|
|
||||||
- not example2_result.changed
|
|
||||||
- example2_result.backup_file is undefined
|
|
||||||
- example2_result.backup_full_chain_file is undefined
|
|
||||||
- example2_result.serial_number == example1_result.serial_number
|
|
||||||
- example2_result.tracking_id == example1_result.tracking_id
|
|
||||||
|
|
||||||
# Internal CA refuses to issue certificates with the same DN in a short time frame
|
|
||||||
- name: Sleep for 5 seconds so we don't run into duplicate-request errors
|
|
||||||
pause:
|
|
||||||
seconds: 5
|
|
||||||
|
|
||||||
- name: Force a reissue with no CSR, verify that contents changed
|
|
||||||
ecs_certificate:
|
|
||||||
backup: True
|
|
||||||
force: True
|
|
||||||
path: '{{ example1_cert_path }}'
|
|
||||||
full_chain_path: '{{ example1_chain_path }}'
|
|
||||||
cert_type: '{{ example1_cert_type }}'
|
|
||||||
request_type: reissue
|
|
||||||
requester_name: '{{ entrust_requester_name }}'
|
|
||||||
requester_email: '{{ entrust_requester_email }}'
|
|
||||||
requester_phone: '{{ entrust_requester_phone }}'
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: example3_result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- example3_result is not failed
|
|
||||||
- example3_result.changed
|
|
||||||
- example3_result.backup_file is string
|
|
||||||
- example3_result.backup_full_chain_file is string
|
|
||||||
- example3_result.tracking_id > 0
|
|
||||||
- example3_result.tracking_id != example1_result.tracking_id
|
|
||||||
- example3_result.serial_number != example1_result.serial_number
|
|
||||||
|
|
||||||
# Internal CA refuses to issue certificates with the same DN in a short time frame
|
|
||||||
- name: Sleep for 5 seconds so we don't run into duplicate-request errors
|
|
||||||
pause:
|
|
||||||
seconds: 5
|
|
||||||
|
|
||||||
- name: Test a request with all of the various optional possible fields populated
|
|
||||||
ecs_certificate:
|
|
||||||
path: '{{ example4_cert_path }}'
|
|
||||||
full_chain_path: '{{ example4_full_chain_path }}'
|
|
||||||
csr: '{{ csr_path }}'
|
|
||||||
subject_alt_name: '{{ example4_subject_alt_name }}'
|
|
||||||
eku: '{{ example4_eku }}'
|
|
||||||
ct_log: True
|
|
||||||
cert_type: '{{ example4_cert_type }}'
|
|
||||||
org: '{{ example4_org }}'
|
|
||||||
ou: '{{ example4_ou }}'
|
|
||||||
tracking_info: '{{ example4_tracking_info }}'
|
|
||||||
additional_emails: '{{ example4_additional_emails }}'
|
|
||||||
custom_fields: '{{ example4_custom_fields }}'
|
|
||||||
cert_expiry: '{{ example4_cert_expiry }}'
|
|
||||||
requester_name: '{{ entrust_requester_name }}'
|
|
||||||
requester_email: '{{ entrust_requester_email }}'
|
|
||||||
requester_phone: '{{ entrust_requester_phone }}'
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: example4_result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- example4_result is not failed
|
|
||||||
- example4_result.changed
|
|
||||||
- example4_result.backup_file is undefined
|
|
||||||
- example4_result.backup_full_chain_file is undefined
|
|
||||||
- example4_result.tracking_id > 0
|
|
||||||
- example4_result.serial_number is string
|
|
||||||
|
|
||||||
# For bug 61738, verify that the full chain is valid
|
|
||||||
- name: Verify that the full chain path can be successfully imported
|
|
||||||
command: openssl verify "{{ example4_full_chain_path }}"
|
|
||||||
register: openssl_result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- "' OK' in openssl_result.stdout_lines[0]"
|
|
||||||
|
|
||||||
always:
|
|
||||||
- name: clean-up temporary folder
|
|
||||||
file:
|
|
||||||
path: '{{ tmpdir_path }}'
|
|
||||||
state: absent
|
|
@ -1,52 +0,0 @@
|
|||||||
---
|
|
||||||
# vars file for test_ecs_certificate
|
|
||||||
|
|
||||||
# Path on various hosts that cacerts need to be put as a prerequisite to API server cert validation.
|
|
||||||
# May need to be customized for some environments based on SSL implementations
|
|
||||||
# that ansible "urls" module utility is using as a backing.
|
|
||||||
cacerts_bundle_path: /etc/pki/tls/certs
|
|
||||||
|
|
||||||
common_name: '{{ ansible_date_time.epoch }}.ansint.testcertificates.com'
|
|
||||||
organization_name: CMS API, Inc.
|
|
||||||
organizational_unit_name: RSA
|
|
||||||
country_name: US
|
|
||||||
state_or_province_name: MA
|
|
||||||
privatekey_passphrase: Passphrase452!
|
|
||||||
tmpdir_path: /tmp/ecs_cert_test/{{ ansible_date_time.epoch }}
|
|
||||||
privatekey_path: '{{ tmpdir_path }}/testcertificates.key'
|
|
||||||
entrust_api_cert: '{{ tmpdir_path }}/authcert.cer'
|
|
||||||
entrust_api_cert_key: '{{ tmpdir_path }}/authkey.cer'
|
|
||||||
csr_path: '{{ tmpdir_path }}/request.csr'
|
|
||||||
|
|
||||||
entrust_requester_name: C Trufan
|
|
||||||
entrust_requester_email: CTIntegrationTests@entrustdatacard.com
|
|
||||||
entrust_requester_phone: 1-555-555-5555 # e.g. 15555555555
|
|
||||||
|
|
||||||
# TEST 1
|
|
||||||
example1_cert_path: '{{ tmpdir_path }}/issuedcert_1.pem'
|
|
||||||
example1_chain_path: '{{ tmpdir_path }}/issuedcert_1_chain.pem'
|
|
||||||
example1_cert_type: EV_SSL
|
|
||||||
|
|
||||||
example4_cert_path: '{{ tmpdir_path }}/issuedcert_2.pem'
|
|
||||||
example4_subject_alt_name:
|
|
||||||
- ansible.testcertificates.com
|
|
||||||
- www.testcertificates.com
|
|
||||||
example4_eku: SERVER_AND_CLIENT_AUTH
|
|
||||||
example4_cert_type: UC_SSL
|
|
||||||
# Test a secondary org and special characters
|
|
||||||
example4_org: Cañon City, Inc.
|
|
||||||
example4_ou:
|
|
||||||
- StringrsaString
|
|
||||||
example4_tracking_info: Submitted via Ansible Integration
|
|
||||||
example4_additional_emails:
|
|
||||||
- itsupport@testcertificates.com
|
|
||||||
- jsmith@ansible.com
|
|
||||||
example4_custom_fields:
|
|
||||||
text1: Admin
|
|
||||||
text2: Invoice 25
|
|
||||||
number1: 342
|
|
||||||
date3: '2018-01-01'
|
|
||||||
email2: sales@ansible.testcertificates.com
|
|
||||||
dropdown2: Dropdown 2 Value 1
|
|
||||||
example4_cert_expiry: 2020-08-15
|
|
||||||
example4_full_chain_path: '{{ tmpdir_path }}/issuedcert_2_chain.pem'
|
|
@ -1,15 +0,0 @@
|
|||||||
# Not enabled due to lack of access to test environments. May be enabled using custom integration_config.yml
|
|
||||||
# Example integation_config.yml
|
|
||||||
# ---
|
|
||||||
# entrust_api_user:
|
|
||||||
# entrust_api_key:
|
|
||||||
# entrust_api_client_cert_path: /var/integration-testing/publicCert.pem
|
|
||||||
# entrust_api_client_cert_key_path: /var/integration-testing/privateKey.pem
|
|
||||||
# entrust_api_ip_address: 127.0.0.1
|
|
||||||
# entrust_cloud_ip_address: 127.0.0.1
|
|
||||||
# # Used for certificate path validation of QA environments - we chose not to support disabling path validation ever.
|
|
||||||
# cacerts_bundle_path_local: /var/integration-testing/cacerts
|
|
||||||
|
|
||||||
### WARNING: This test will update HOSTS file and CERTIFICATE STORE of target host, in order to be able to validate
|
|
||||||
# to a QA environment. ###
|
|
||||||
unsupported
|
|
@ -1,2 +0,0 @@
|
|||||||
---
|
|
||||||
# defaults file for test_ecs_domain
|
|
@ -1,2 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- prepare_tests
|
|
@ -1,270 +0,0 @@
|
|||||||
---
|
|
||||||
## Verify that integration_config was specified
|
|
||||||
- block:
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- entrust_api_user is defined
|
|
||||||
- entrust_api_key is defined
|
|
||||||
- entrust_api_ip_address is defined
|
|
||||||
- entrust_cloud_ip_address is defined
|
|
||||||
- entrust_api_client_cert_path is defined or entrust_api_client_cert_contents is defined
|
|
||||||
- entrust_api_client_cert_key_path is defined or entrust_api_client_cert_key_contents
|
|
||||||
- cacerts_bundle_path_local is defined
|
|
||||||
|
|
||||||
## SET UP TEST ENVIRONMENT ########################################################################
|
|
||||||
- name: copy the files needed for verifying test server certificate to the host
|
|
||||||
copy:
|
|
||||||
src: '{{ cacerts_bundle_path_local }}/'
|
|
||||||
dest: '{{ cacerts_bundle_path }}'
|
|
||||||
|
|
||||||
- name: Update the CA certificates for our QA certs (collection may need updating if new QA environments used)
|
|
||||||
command: c_rehash {{ cacerts_bundle_path }}
|
|
||||||
|
|
||||||
- name: Update hosts file
|
|
||||||
lineinfile:
|
|
||||||
path: /etc/hosts
|
|
||||||
state: present
|
|
||||||
regexp: 'api.entrust.net$'
|
|
||||||
line: '{{ entrust_api_ip_address }} api.entrust.net'
|
|
||||||
|
|
||||||
- name: Update hosts file
|
|
||||||
lineinfile:
|
|
||||||
path: /etc/hosts
|
|
||||||
state: present
|
|
||||||
regexp: 'cloud.entrust.net$'
|
|
||||||
line: '{{ entrust_cloud_ip_address }} cloud.entrust.net'
|
|
||||||
|
|
||||||
- name: Clear out the temporary directory for storing the API connection information
|
|
||||||
file:
|
|
||||||
path: '{{ tmpdir_path }}'
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Create a directory for storing the API connection Information
|
|
||||||
file:
|
|
||||||
path: '{{ tmpdir_path }}'
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- name: Copy the files needed for the connection to entrust API to the host
|
|
||||||
copy:
|
|
||||||
src: '{{ entrust_api_client_cert_path }}'
|
|
||||||
dest: '{{ entrust_api_cert }}'
|
|
||||||
|
|
||||||
- name: Copy the files needed for the connection to entrust API to the host
|
|
||||||
copy:
|
|
||||||
src: '{{ entrust_api_client_cert_key_path }}'
|
|
||||||
dest: '{{ entrust_api_cert_key }}'
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: Have ECS request a domain validation via dns
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: dns.{{ common_name }}
|
|
||||||
verification_method: dns
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: dns_result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- dns_result is not failed
|
|
||||||
- dns_result.changed
|
|
||||||
- dns_result.domain_status == 'INITIAL_VERIFICATION'
|
|
||||||
- dns_result.verification_method == 'dns'
|
|
||||||
- dns_result.dns_location is string
|
|
||||||
- dns_result.dns_contents is string
|
|
||||||
- dns_result.dns_resource_type is string
|
|
||||||
- dns_result.file_location is undefined
|
|
||||||
- dns_result.file_contents is undefined
|
|
||||||
- dns_result.emails is undefined
|
|
||||||
|
|
||||||
- name: Have ECS request a domain validation via web_server
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: FILE.{{ common_name }}
|
|
||||||
verification_method: web_server
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: file_result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- file_result is not failed
|
|
||||||
- file_result.changed
|
|
||||||
- file_result.domain_status == 'INITIAL_VERIFICATION'
|
|
||||||
- file_result.verification_method == 'web_server'
|
|
||||||
- file_result.dns_location is undefined
|
|
||||||
- file_result.dns_contents is undefined
|
|
||||||
- file_result.dns_resource_type is undefined
|
|
||||||
- file_result.file_location is string
|
|
||||||
- file_result.file_contents is string
|
|
||||||
- file_result.emails is undefined
|
|
||||||
|
|
||||||
- name: Have ECS request a domain validation via email
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: email.{{ common_name }}
|
|
||||||
verification_method: email
|
|
||||||
verification_email: admin@testcertificates.com
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: email_result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- email_result is not failed
|
|
||||||
- email_result.changed
|
|
||||||
- email_result.domain_status == 'INITIAL_VERIFICATION'
|
|
||||||
- email_result.verification_method == 'email'
|
|
||||||
- email_result.dns_location is undefined
|
|
||||||
- email_result.dns_contents is undefined
|
|
||||||
- email_result.dns_resource_type is undefined
|
|
||||||
- email_result.file_location is undefined
|
|
||||||
- email_result.file_contents is undefined
|
|
||||||
- email_result.emails[0] == 'admin@testcertificates.com'
|
|
||||||
|
|
||||||
- name: Have ECS request a domain validation via email with no address provided
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: email2.{{ common_name }}
|
|
||||||
verification_method: email
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: email_result2
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- email_result2 is not failed
|
|
||||||
- email_result2.changed
|
|
||||||
- email_result2.domain_status == 'INITIAL_VERIFICATION'
|
|
||||||
- email_result2.verification_method == 'email'
|
|
||||||
- email_result2.dns_location is undefined
|
|
||||||
- email_result2.dns_contents is undefined
|
|
||||||
- email_result2.dns_resource_type is undefined
|
|
||||||
- email_result2.file_location is undefined
|
|
||||||
- email_result2.file_contents is undefined
|
|
||||||
- email_result2.emails is defined
|
|
||||||
|
|
||||||
- name: Have ECS request a domain validation via manual
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: manual.{{ common_name }}
|
|
||||||
verification_method: manual
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: manual_result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- manual_result is not failed
|
|
||||||
- manual_result.changed
|
|
||||||
- manual_result.domain_status == 'INITIAL_VERIFICATION'
|
|
||||||
- manual_result.verification_method == 'manual'
|
|
||||||
- manual_result.dns_location is undefined
|
|
||||||
- manual_result.dns_contents is undefined
|
|
||||||
- manual_result.dns_resource_type is undefined
|
|
||||||
- manual_result.file_location is undefined
|
|
||||||
- manual_result.file_contents is undefined
|
|
||||||
- manual_result.emails is undefined
|
|
||||||
|
|
||||||
- name: Have ECS request a domain validation via dns that remains unchanged
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: dns.{{ common_name }}
|
|
||||||
verification_method: dns
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: dns_result2
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- dns_result2 is not failed
|
|
||||||
- not dns_result2.changed
|
|
||||||
- dns_result2.domain_status == 'INITIAL_VERIFICATION'
|
|
||||||
- dns_result2.verification_method == 'dns'
|
|
||||||
- dns_result2.dns_location is string
|
|
||||||
- dns_result2.dns_contents is string
|
|
||||||
- dns_result2.dns_resource_type is string
|
|
||||||
- dns_result2.file_location is undefined
|
|
||||||
- dns_result2.file_contents is undefined
|
|
||||||
- dns_result2.emails is undefined
|
|
||||||
|
|
||||||
- name: Have ECS request a domain validation via FILE for dns, to change verification method
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: dns.{{ common_name }}
|
|
||||||
verification_method: web_server
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: dns_result_now_file
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- dns_result_now_file is not failed
|
|
||||||
- dns_result_now_file.changed
|
|
||||||
- dns_result_now_file.domain_status == 'INITIAL_VERIFICATION'
|
|
||||||
- dns_result_now_file.verification_method == 'web_server'
|
|
||||||
- dns_result_now_file.dns_location is undefined
|
|
||||||
- dns_result_now_file.dns_contents is undefined
|
|
||||||
- dns_result_now_file.dns_resource_type is undefined
|
|
||||||
- dns_result_now_file.file_location is string
|
|
||||||
- dns_result_now_file.file_contents is string
|
|
||||||
- dns_result_now_file.emails is undefined
|
|
||||||
|
|
||||||
- name: Request revalidation of an approved domain
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: '{{ existing_domain_common_name }}'
|
|
||||||
verification_method: manual
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: manual_existing_domain
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- manual_existing_domain is not failed
|
|
||||||
- not manual_existing_domain.changed
|
|
||||||
- manual_existing_domain.domain_status == 'RE_VERIFICATION'
|
|
||||||
- manual_existing_domain.dns_location is undefined
|
|
||||||
- manual_existing_domain.dns_contents is undefined
|
|
||||||
- manual_existing_domain.dns_resource_type is undefined
|
|
||||||
- manual_existing_domain.file_location is undefined
|
|
||||||
- manual_existing_domain.file_contents is undefined
|
|
||||||
- manual_existing_domain.emails is undefined
|
|
||||||
|
|
||||||
- name: Request revalidation of an approved domain
|
|
||||||
ecs_domain:
|
|
||||||
domain_name: '{{ existing_domain_common_name }}'
|
|
||||||
verification_method: web_server
|
|
||||||
entrust_api_user: '{{ entrust_api_user }}'
|
|
||||||
entrust_api_key: '{{ entrust_api_key }}'
|
|
||||||
entrust_api_client_cert_path: '{{ entrust_api_cert }}'
|
|
||||||
entrust_api_client_cert_key_path: '{{ entrust_api_cert_key }}'
|
|
||||||
register: file_existing_domain_revalidate
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- file_existing_domain_revalidate is not failed
|
|
||||||
- file_existing_domain_revalidate.changed
|
|
||||||
- file_existing_domain_revalidate.domain_status == 'RE_VERIFICATION'
|
|
||||||
- file_existing_domain_revalidate.verification_method == 'web_server'
|
|
||||||
- file_existing_domain_revalidate.dns_location is undefined
|
|
||||||
- file_existing_domain_revalidate.dns_contents is undefined
|
|
||||||
- file_existing_domain_revalidate.dns_resource_type is undefined
|
|
||||||
- file_existing_domain_revalidate.file_location is string
|
|
||||||
- file_existing_domain_revalidate.file_contents is string
|
|
||||||
- file_existing_domain_revalidate.emails is undefined
|
|
||||||
|
|
||||||
|
|
||||||
always:
|
|
||||||
- name: clean-up temporary folder
|
|
||||||
file:
|
|
||||||
path: '{{ tmpdir_path }}'
|
|
||||||
state: absent
|
|
@ -1,15 +0,0 @@
|
|||||||
---
|
|
||||||
# vars file for test_ecs_certificate
|
|
||||||
|
|
||||||
# Path on various hosts that cacerts need to be put as a prerequisite to API server cert validation.
|
|
||||||
# May need to be customized for some environments based on SSL implementations
|
|
||||||
# that ansible "urls" module utility is using as a backing.
|
|
||||||
cacerts_bundle_path: /etc/pki/tls/certs
|
|
||||||
|
|
||||||
common_name: '{{ ansible_date_time.epoch }}.testcertificates.com'
|
|
||||||
existing_domain_common_name: 'testcertificates.com'
|
|
||||||
|
|
||||||
tmpdir_path: /tmp/ecs_cert_test/{{ ansible_date_time.epoch }}
|
|
||||||
|
|
||||||
entrust_api_cert: '{{ tmpdir_path }}/authcert.cer'
|
|
||||||
entrust_api_cert_key: '{{ tmpdir_path }}/authkey.cer'
|
|
@ -1,4 +0,0 @@
|
|||||||
shippable/posix/group1
|
|
||||||
destructive
|
|
||||||
needs/httptester
|
|
||||||
skip/aix
|
|
@ -1,18 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIC+DCCAeACCQCWuDvGDH3otTANBgkqhkiG9w0BAQsFADA+MQswCQYDVQQGEwJV
|
|
||||||
UzEOMAwGA1UECAwFQm9ndXMxEDAOBgNVBAcMB0JhbG9uZXkxDTALBgNVBAoMBEFD
|
|
||||||
TUUwHhcNMTgwNzEyMTgxNDA0WhcNMjMwNzExMTgxNDA0WjA+MQswCQYDVQQGEwJV
|
|
||||||
UzEOMAwGA1UECAwFQm9ndXMxEDAOBgNVBAcMB0JhbG9uZXkxDTALBgNVBAoMBEFD
|
|
||||||
TUUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLTGCpn8b+/2qdpkvK
|
|
||||||
iwXU8PMOXBOmRa+GmzxsxMr1QZcY0m6pY3uuIvqErMFf4qp4BMxQF+VpDLVJUJX/
|
|
||||||
1oKCM7J3hEfgmKRD4RmKhBlnWVv5YGZmvlXRJBl1AsDTONZy8iKJB5NYnB3ZyrJq
|
|
||||||
H2GAgyJ55aYckoU55vwjRzKp49dZmzX5YS04Kzzzw/SmOuW8kMypZV5TJH+NXqKc
|
|
||||||
pw3u3cJ4yJ9DHSU5pnhC5BeKl8XDMO42jRWt5/7C7JDiCbZ9lu5jQiv/4DhsRsHF
|
|
||||||
A8/Lgl47sNDaBMbha786I9laPHLlVycpYaP6pwtizhN9ZRTdDOHmWi/vjiamERLL
|
|
||||||
FjjLAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAA+1uj3tHaCai+A1H/kOgTN5e0eW
|
|
||||||
/wmaxu8gNK5eiHrecNJNAlFxVTrCwhvv4nUW7NXVcW/1WUqSO0QMiPJhCsSLVAMF
|
|
||||||
8MuYH73B+ctRqAGdeOAWF+ftCywZTEj5h5F0XiWB+TmkPlTVNShMiPFelDJpLy7u
|
|
||||||
9MfiPEJjo4sZotQl8/pZ6R9cY6GpEXWnttcuhLJCEuiB8fWO7epiWYCt/Ak+CVmZ
|
|
||||||
OzfI/euV6Upaen22lNu8V3ZwWEFtmU5CioKJ3S8DK5Mw/LJIJw1ZY9E+fTtn8x0k
|
|
||||||
xlI4e7urD2FYhTdv2fFUG8Z5arb/3bICgsUYQZ+G1c3wjWtJg9zcy8hpnZQ=
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -1,28 +0,0 @@
|
|||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
from sys import argv
|
|
||||||
from subprocess import Popen, PIPE, STDOUT
|
|
||||||
|
|
||||||
p = Popen(["openssl", "s_client", "-host", argv[1], "-port", "443", "-prexit", "-showcerts"], stdin=PIPE, stdout=PIPE, stderr=STDOUT)
|
|
||||||
stdout = p.communicate(input=b'\n')[0]
|
|
||||||
data = stdout.decode()
|
|
||||||
|
|
||||||
certs = []
|
|
||||||
cert = ""
|
|
||||||
capturing = False
|
|
||||||
for line in data.split('\n'):
|
|
||||||
if line == '-----BEGIN CERTIFICATE-----':
|
|
||||||
capturing = True
|
|
||||||
|
|
||||||
if capturing:
|
|
||||||
cert = "{0}{1}\n".format(cert, line)
|
|
||||||
|
|
||||||
if line == '-----END CERTIFICATE-----':
|
|
||||||
capturing = False
|
|
||||||
certs.append(cert)
|
|
||||||
cert = ""
|
|
||||||
|
|
||||||
with open(argv[2], 'w') as f:
|
|
||||||
for cert in set(certs):
|
|
||||||
f.write(cert)
|
|
@ -1,3 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- setup_openssl
|
|
||||||
- prepare_http_tests
|
|
@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
- block:
|
|
||||||
|
|
||||||
- name: Get servers certificate with backend auto-detection
|
|
||||||
get_certificate:
|
|
||||||
host: "{{ httpbin_host }}"
|
|
||||||
port: 443
|
|
||||||
|
|
||||||
when: |
|
|
||||||
pyopenssl_version.stdout is version('0.15', '>=') or
|
|
||||||
(cryptography_version.stdout is version('1.6', '>=') and (ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6))
|
|
||||||
|
|
||||||
- block:
|
|
||||||
|
|
||||||
- include_tasks: ../tests/validate.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: pyopenssl
|
|
||||||
|
|
||||||
when: pyopenssl_version.stdout is version('0.15', '>=')
|
|
||||||
|
|
||||||
- name: Remove output directory
|
|
||||||
file:
|
|
||||||
path: "{{ output_dir }}"
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Re-create output directory
|
|
||||||
file:
|
|
||||||
path: "{{ output_dir }}"
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- block:
|
|
||||||
|
|
||||||
- include_tasks: ../tests/validate.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: cryptography
|
|
||||||
|
|
||||||
# The module doesn't work with CentOS 6. Since the pyOpenSSL installed there is too old,
|
|
||||||
# we never noticed before. This becomes a problem with the new cryptography backend,
|
|
||||||
# since there is a new enough cryptography version...
|
|
||||||
when: |
|
|
||||||
cryptography_version.stdout is version('1.6', '>=') and
|
|
||||||
(ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6)
|
|
@ -1,106 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Get servers certificate
|
|
||||||
get_certificate:
|
|
||||||
host: "{{ httpbin_host }}"
|
|
||||||
port: 443
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- debug: var=result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
# This module should never change anything
|
|
||||||
- result is not changed
|
|
||||||
- result is not failed
|
|
||||||
# We got the correct ST from the cert
|
|
||||||
- "'North Carolina' == result.subject.ST"
|
|
||||||
|
|
||||||
- name: Connect to http port (will fail because there is no SSL cert to get)
|
|
||||||
get_certificate:
|
|
||||||
host: "{{ httpbin_host }}"
|
|
||||||
port: 80
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
register: result
|
|
||||||
ignore_errors: true
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- result is not changed
|
|
||||||
- result is failed
|
|
||||||
# We got the expected error message
|
|
||||||
- "'The handshake operation timed out' in result.msg or 'unknown protocol' in result.msg or 'wrong version number' in result.msg"
|
|
||||||
|
|
||||||
- name: Test timeout option
|
|
||||||
get_certificate:
|
|
||||||
host: "{{ httpbin_host }}"
|
|
||||||
port: 1234
|
|
||||||
timeout: 1
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
register: result
|
|
||||||
ignore_errors: true
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- result is not changed
|
|
||||||
- result is failed
|
|
||||||
# We got the expected error message
|
|
||||||
- "'Failed to get cert from port with error: timed out' == result.msg or 'Connection refused' in result.msg"
|
|
||||||
|
|
||||||
- name: Test failure if ca_cert is not a valid file
|
|
||||||
get_certificate:
|
|
||||||
host: "{{ httpbin_host }}"
|
|
||||||
port: 443
|
|
||||||
ca_cert: dn.e
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
register: result
|
|
||||||
ignore_errors: true
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- result is not changed
|
|
||||||
- result is failed
|
|
||||||
# We got the correct response from the module
|
|
||||||
- "'ca_cert file does not exist' == result.msg"
|
|
||||||
|
|
||||||
- name: Download CA Cert as pem from server
|
|
||||||
get_url:
|
|
||||||
url: "http://ansible.http.tests/cacert.pem"
|
|
||||||
dest: "{{ output_dir }}/temp.pem"
|
|
||||||
|
|
||||||
- name: Get servers certificate comparing it to its own ca_cert file
|
|
||||||
get_certificate:
|
|
||||||
ca_cert: '{{ output_dir }}/temp.pem'
|
|
||||||
host: "{{ httpbin_host }}"
|
|
||||||
port: 443
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- result is not changed
|
|
||||||
- result is not failed
|
|
||||||
|
|
||||||
- name: Get a temp directory
|
|
||||||
tempfile:
|
|
||||||
state: directory
|
|
||||||
register: my_temp_dir
|
|
||||||
|
|
||||||
- name: Deploy the bogus_ca.pem file
|
|
||||||
copy:
|
|
||||||
src: "bogus_ca.pem"
|
|
||||||
dest: "{{ my_temp_dir.path }}/bogus_ca.pem"
|
|
||||||
|
|
||||||
- name: Get servers certificate comparing it to an invalid ca_cert file
|
|
||||||
get_certificate:
|
|
||||||
ca_cert: '{{ my_temp_dir.path }}/bogus_ca.pem'
|
|
||||||
host: "{{ httpbin_host }}"
|
|
||||||
port: 443
|
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
|
||||||
register: result
|
|
||||||
ignore_errors: true
|
|
||||||
|
|
||||||
- assert:
|
|
||||||
that:
|
|
||||||
- result is not changed
|
|
||||||
- result.failed
|
|
@ -1,4 +0,0 @@
|
|||||||
x509_crl_info
|
|
||||||
shippable/posix/incidental
|
|
||||||
destructive
|
|
||||||
skip/aix
|
|
@ -1,2 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- incidental_setup_openssl
|
|
@ -1,289 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Create CRL 1 (check mode)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl1.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: 20191013000000Z
|
|
||||||
next_update: 20191113000000Z
|
|
||||||
revoked_certificates:
|
|
||||||
- path: '{{ output_dir }}/cert-1.pem'
|
|
||||||
revocation_date: 20191013000000Z
|
|
||||||
- path: '{{ output_dir }}/cert-2.pem'
|
|
||||||
revocation_date: 20191013000000Z
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
- serial_number: 1234
|
|
||||||
revocation_date: 20191001000000Z
|
|
||||||
check_mode: yes
|
|
||||||
register: crl_1_check
|
|
||||||
- name: Create CRL 1
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl1.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: 20191013000000Z
|
|
||||||
next_update: 20191113000000Z
|
|
||||||
revoked_certificates:
|
|
||||||
- path: '{{ output_dir }}/cert-1.pem'
|
|
||||||
revocation_date: 20191013000000Z
|
|
||||||
- path: '{{ output_dir }}/cert-2.pem'
|
|
||||||
revocation_date: 20191013000000Z
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
- serial_number: 1234
|
|
||||||
revocation_date: 20191001000000Z
|
|
||||||
register: crl_1
|
|
||||||
- name: Retrieve CRL 1 infos
|
|
||||||
x509_crl_info:
|
|
||||||
path: '{{ output_dir }}/ca-crl1.crl'
|
|
||||||
register: crl_1_info_1
|
|
||||||
- name: Retrieve CRL 1 infos via file content
|
|
||||||
x509_crl_info:
|
|
||||||
content: '{{ lookup("file", output_dir ~ "/ca-crl1.crl") }}'
|
|
||||||
register: crl_1_info_2
|
|
||||||
- name: Create CRL 1 (idempotent, check mode)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl1.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: 20191013000000Z
|
|
||||||
next_update: 20191113000000Z
|
|
||||||
revoked_certificates:
|
|
||||||
- path: '{{ output_dir }}/cert-1.pem'
|
|
||||||
revocation_date: 20191013000000Z
|
|
||||||
- path: '{{ output_dir }}/cert-2.pem'
|
|
||||||
revocation_date: 20191013000000Z
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
- serial_number: 1234
|
|
||||||
revocation_date: 20191001000000Z
|
|
||||||
check_mode: yes
|
|
||||||
register: crl_1_idem_check
|
|
||||||
- name: Create CRL 1 (idempotent)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl1.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: 20191013000000Z
|
|
||||||
next_update: 20191113000000Z
|
|
||||||
revoked_certificates:
|
|
||||||
- path: '{{ output_dir }}/cert-1.pem'
|
|
||||||
revocation_date: 20191013000000Z
|
|
||||||
- path: '{{ output_dir }}/cert-2.pem'
|
|
||||||
revocation_date: 20191013000000Z
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
- serial_number: 1234
|
|
||||||
revocation_date: 20191001000000Z
|
|
||||||
register: crl_1_idem
|
|
||||||
- name: Create CRL 1 (idempotent with content, check mode)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl1.crl'
|
|
||||||
privatekey_content: "{{ lookup('file', output_dir ~ '/ca.key') }}"
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: 20191013000000Z
|
|
||||||
next_update: 20191113000000Z
|
|
||||||
revoked_certificates:
|
|
||||||
- content: "{{ lookup('file', output_dir ~ '/cert-1.pem') }}"
|
|
||||||
revocation_date: 20191013000000Z
|
|
||||||
- content: "{{ lookup('file', output_dir ~ '/cert-2.pem') }}"
|
|
||||||
revocation_date: 20191013000000Z
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
- serial_number: 1234
|
|
||||||
revocation_date: 20191001000000Z
|
|
||||||
check_mode: yes
|
|
||||||
register: crl_1_idem_content_check
|
|
||||||
- name: Create CRL 1 (idempotent with content)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl1.crl'
|
|
||||||
privatekey_content: "{{ lookup('file', output_dir ~ '/ca.key') }}"
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: 20191013000000Z
|
|
||||||
next_update: 20191113000000Z
|
|
||||||
revoked_certificates:
|
|
||||||
- content: "{{ lookup('file', output_dir ~ '/cert-1.pem') }}"
|
|
||||||
revocation_date: 20191013000000Z
|
|
||||||
- content: "{{ lookup('file', output_dir ~ '/cert-2.pem') }}"
|
|
||||||
revocation_date: 20191013000000Z
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
- serial_number: 1234
|
|
||||||
revocation_date: 20191001000000Z
|
|
||||||
register: crl_1_idem_content
|
|
||||||
|
|
||||||
- name: Create CRL 2 (check mode)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl2.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: +0d
|
|
||||||
next_update: +0d
|
|
||||||
revoked_certificates:
|
|
||||||
- path: '{{ output_dir }}/cert-1.pem'
|
|
||||||
- path: '{{ output_dir }}/cert-2.pem'
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
- serial_number: 1234
|
|
||||||
check_mode: yes
|
|
||||||
register: crl_2_check
|
|
||||||
- name: Create CRL 2
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl2.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: +0d
|
|
||||||
next_update: +0d
|
|
||||||
revoked_certificates:
|
|
||||||
- path: '{{ output_dir }}/cert-1.pem'
|
|
||||||
- path: '{{ output_dir }}/cert-2.pem'
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
- serial_number: 1234
|
|
||||||
register: crl_2
|
|
||||||
- name: Create CRL 2 (idempotent, check mode)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl2.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: +0d
|
|
||||||
next_update: +0d
|
|
||||||
revoked_certificates:
|
|
||||||
- path: '{{ output_dir }}/cert-1.pem'
|
|
||||||
- path: '{{ output_dir }}/cert-2.pem'
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
- serial_number: 1234
|
|
||||||
ignore_timestamps: yes
|
|
||||||
check_mode: yes
|
|
||||||
register: crl_2_idem_check
|
|
||||||
- name: Create CRL 2 (idempotent)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl2.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: +0d
|
|
||||||
next_update: +0d
|
|
||||||
revoked_certificates:
|
|
||||||
- path: '{{ output_dir }}/cert-1.pem'
|
|
||||||
- path: '{{ output_dir }}/cert-2.pem'
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
- serial_number: 1234
|
|
||||||
ignore_timestamps: yes
|
|
||||||
register: crl_2_idem
|
|
||||||
- name: Create CRL 2 (idempotent update, check mode)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl2.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: +0d
|
|
||||||
next_update: +0d
|
|
||||||
revoked_certificates:
|
|
||||||
- serial_number: 1235
|
|
||||||
ignore_timestamps: yes
|
|
||||||
mode: update
|
|
||||||
check_mode: yes
|
|
||||||
register: crl_2_idem_update_change_check
|
|
||||||
- name: Create CRL 2 (idempotent update)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl2.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: +0d
|
|
||||||
next_update: +0d
|
|
||||||
revoked_certificates:
|
|
||||||
- serial_number: 1235
|
|
||||||
ignore_timestamps: yes
|
|
||||||
mode: update
|
|
||||||
register: crl_2_idem_update_change
|
|
||||||
- name: Create CRL 2 (idempotent update, check mode)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl2.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: +0d
|
|
||||||
next_update: +0d
|
|
||||||
revoked_certificates:
|
|
||||||
- path: '{{ output_dir }}/cert-2.pem'
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
ignore_timestamps: yes
|
|
||||||
mode: update
|
|
||||||
check_mode: yes
|
|
||||||
register: crl_2_idem_update_check
|
|
||||||
- name: Create CRL 2 (idempotent update)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl2.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: +0d
|
|
||||||
next_update: +0d
|
|
||||||
revoked_certificates:
|
|
||||||
- path: '{{ output_dir }}/cert-2.pem'
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
ignore_timestamps: yes
|
|
||||||
mode: update
|
|
||||||
register: crl_2_idem_update
|
|
||||||
- name: Create CRL 2 (changed timestamps, check mode)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl2.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: +0d
|
|
||||||
next_update: +0d
|
|
||||||
revoked_certificates:
|
|
||||||
- path: '{{ output_dir }}/cert-2.pem'
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
ignore_timestamps: no
|
|
||||||
mode: update
|
|
||||||
check_mode: yes
|
|
||||||
register: crl_2_change_check
|
|
||||||
- name: Create CRL 2 (changed timestamps)
|
|
||||||
x509_crl:
|
|
||||||
path: '{{ output_dir }}/ca-crl2.crl'
|
|
||||||
privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
issuer:
|
|
||||||
CN: Ansible
|
|
||||||
last_update: +0d
|
|
||||||
next_update: +0d
|
|
||||||
revoked_certificates:
|
|
||||||
- path: '{{ output_dir }}/cert-2.pem'
|
|
||||||
reason: key_compromise
|
|
||||||
reason_critical: yes
|
|
||||||
invalidity_date: 20191012000000Z
|
|
||||||
ignore_timestamps: no
|
|
||||||
mode: update
|
|
||||||
return_content: yes
|
|
||||||
register: crl_2_change
|
|
@ -1,83 +0,0 @@
|
|||||||
---
|
|
||||||
- set_fact:
|
|
||||||
certificates:
|
|
||||||
- name: ca
|
|
||||||
subject:
|
|
||||||
commonName: Ansible
|
|
||||||
is_ca: yes
|
|
||||||
- name: ca-2
|
|
||||||
subject:
|
|
||||||
commonName: Ansible Other CA
|
|
||||||
is_ca: yes
|
|
||||||
- name: cert-1
|
|
||||||
subject_alt_name:
|
|
||||||
- DNS:ansible.com
|
|
||||||
- name: cert-2
|
|
||||||
subject_alt_name:
|
|
||||||
- DNS:example.com
|
|
||||||
- name: cert-3
|
|
||||||
subject_alt_name:
|
|
||||||
- DNS:example.org
|
|
||||||
- IP:1.2.3.4
|
|
||||||
- name: cert-4
|
|
||||||
subject_alt_name:
|
|
||||||
- DNS:test.ansible.com
|
|
||||||
- DNS:b64.ansible.com
|
|
||||||
|
|
||||||
- name: Generate private keys
|
|
||||||
openssl_privatekey:
|
|
||||||
path: '{{ output_dir }}/{{ item.name }}.key'
|
|
||||||
type: ECC
|
|
||||||
curve: secp256r1
|
|
||||||
loop: "{{ certificates }}"
|
|
||||||
|
|
||||||
- name: Generate CSRs
|
|
||||||
openssl_csr:
|
|
||||||
path: '{{ output_dir }}/{{ item.name }}.csr'
|
|
||||||
privatekey_path: '{{ output_dir }}/{{ item.name }}.key'
|
|
||||||
subject: "{{ item.subject | default(omit) }}"
|
|
||||||
subject_alt_name: "{{ item.subject_alt_name | default(omit) }}"
|
|
||||||
basic_constraints: "{{ 'CA:TRUE' if item.is_ca | default(false) else omit }}"
|
|
||||||
use_common_name_for_san: no
|
|
||||||
loop: "{{ certificates }}"
|
|
||||||
|
|
||||||
- name: Generate CA certificates
|
|
||||||
openssl_certificate:
|
|
||||||
path: '{{ output_dir }}/{{ item.name }}.pem'
|
|
||||||
csr_path: '{{ output_dir }}/{{ item.name }}.csr'
|
|
||||||
privatekey_path: '{{ output_dir }}/{{ item.name }}.key'
|
|
||||||
provider: selfsigned
|
|
||||||
loop: "{{ certificates }}"
|
|
||||||
when: item.is_ca | default(false)
|
|
||||||
|
|
||||||
- name: Generate other certificates
|
|
||||||
openssl_certificate:
|
|
||||||
path: '{{ output_dir }}/{{ item.name }}.pem'
|
|
||||||
csr_path: '{{ output_dir }}/{{ item.name }}.csr'
|
|
||||||
provider: ownca
|
|
||||||
ownca_path: '{{ output_dir }}/ca.pem'
|
|
||||||
ownca_privatekey_path: '{{ output_dir }}/ca.key'
|
|
||||||
loop: "{{ certificates }}"
|
|
||||||
when: not (item.is_ca | default(false))
|
|
||||||
|
|
||||||
- name: Get certificate infos
|
|
||||||
openssl_certificate_info:
|
|
||||||
path: '{{ output_dir }}/{{ item }}.pem'
|
|
||||||
loop:
|
|
||||||
- cert-1
|
|
||||||
- cert-2
|
|
||||||
- cert-3
|
|
||||||
- cert-4
|
|
||||||
register: certificate_infos
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: Running tests with cryptography backend
|
|
||||||
include_tasks: impl.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: cryptography
|
|
||||||
|
|
||||||
- import_tasks: ../tests/validate.yml
|
|
||||||
vars:
|
|
||||||
select_crypto_backend: cryptography
|
|
||||||
|
|
||||||
when: cryptography_version.stdout is version('1.2', '>=')
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue