diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/crypto | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/crypto')
490 files changed, 67559 insertions, 0 deletions
diff --git a/ansible_collections/community/crypto/.azure-pipelines/README.md b/ansible_collections/community/crypto/.azure-pipelines/README.md new file mode 100644 index 000000000..9e8ad7410 --- /dev/null +++ b/ansible_collections/community/crypto/.azure-pipelines/README.md @@ -0,0 +1,9 @@ +<!-- +Copyright (c) Ansible Project +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +--> + +## Azure Pipelines Configuration + +Please see the [Documentation](https://github.com/ansible/community/wiki/Testing:-Azure-Pipelines) for more information. diff --git a/ansible_collections/community/crypto/.azure-pipelines/azure-pipelines.yml b/ansible_collections/community/crypto/.azure-pipelines/azure-pipelines.yml new file mode 100644 index 000000000..d9ae3f866 --- /dev/null +++ b/ansible_collections/community/crypto/.azure-pipelines/azure-pipelines.yml @@ -0,0 +1,362 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +trigger: + batch: true + branches: + include: + - main + - stable-* + +pr: + autoCancel: true + branches: + include: + - main + - stable-* + +schedules: + - cron: 0 9 * * * + displayName: Nightly + always: true + branches: + include: + - main + - cron: 0 12 * * 0 + displayName: Weekly (old stable branches) + always: true + branches: + include: + - stable-* + +variables: + - name: checkoutPath + value: ansible_collections/community/crypto + - name: coverageBranches + value: main + - name: pipelinesCoverage + value: coverage + - name: entryPoint + value: tests/utils/shippable/shippable.sh + - name: fetchDepth + value: 0 + +resources: + containers: + - container: default + image: quay.io/ansible/azure-pipelines-test-container:3.0.0 + +pool: Standard + +stages: +### Sanity & units + - stage: Ansible_devel + displayName: Sanity & Units devel + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + targets: + - name: Sanity + test: 'devel/sanity/1' + - name: Sanity Extra # Only on devel + test: 'devel/sanity/extra' + - name: Units + test: 'devel/units/1' + - stage: Ansible_2_15 + displayName: Sanity & Units 2.15 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + targets: + - name: Sanity + test: '2.15/sanity/1' + - name: Units + test: '2.15/units/1' + - stage: Ansible_2_14 + displayName: Sanity & Units 2.14 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + targets: + - name: Sanity + test: '2.14/sanity/1' + - name: Units + test: '2.14/units/1' + - stage: Ansible_2_13 + displayName: Sanity & Units 2.13 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + targets: + - name: Sanity + test: '2.13/sanity/1' + - name: Units + test: '2.13/units/1' +### Docker + - stage: Docker_devel + displayName: Docker devel + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + testFormat: devel/linux/{0} + targets: + - name: Fedora 37 + test: fedora37 + - name: openSUSE 15 + test: opensuse15 + - name: Ubuntu 20.04 + test: ubuntu2004 + - name: Ubuntu 22.04 + test: ubuntu2204 + - name: Alpine 3 + test: alpine3 + groups: + - 1 + - 2 + - stage: Docker_2_15 + displayName: Docker 2.15 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + testFormat: 2.15/linux/{0} + targets: + - name: CentOS 7 + test: centos7 + groups: + - 1 + - 2 + - stage: Docker_2_14 + displayName: Docker 2.14 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + testFormat: 2.14/linux/{0} + targets: + - name: Fedora 36 + test: fedora36 + groups: + - 1 + - 2 + - stage: Docker_2_13 + displayName: Docker 2.13 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + testFormat: 2.13/linux/{0} + targets: + - name: openSUSE 15 py2 + test: opensuse15py2 + - name: Fedora 35 + test: fedora35 + - name: Fedora 34 + test: fedora34 + - name: Ubuntu 18.04 + test: ubuntu1804 + - name: Alpine 3 + test: alpine3 + groups: + - 1 + - 2 + +### Community Docker + - stage: Docker_community_devel + displayName: Docker (community images) devel + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + testFormat: devel/linux-community/{0} + targets: + - name: Debian Bullseye + test: debian-bullseye/3.9 + - name: ArchLinux + test: archlinux/3.11 + - name: CentOS Stream 8 with Python 3.9 + test: centos-stream8/3.9 + - name: CentOS Stream 8 with Python 3.6 + test: centos-stream8/3.6 + groups: + - 1 + - 2 + +### Remote + - stage: Remote_devel_extra_vms + displayName: Remote devel extra VMs + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + testFormat: devel/{0} + targets: + - name: Alpine 3.17 + test: alpine/3.17 + - name: Fedora 37 + test: fedora/37 + - name: Ubuntu 20.04 + test: ubuntu/20.04 + - name: Ubuntu 22.04 + test: ubuntu/22.04 + groups: + - vm + - stage: Remote_devel + displayName: Remote devel + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + testFormat: devel/{0} + targets: + - name: macOS 13.2 + test: macos/13.2 + - name: RHEL 9.1 + test: rhel/9.1 + - name: FreeBSD 12.4 + test: freebsd/12.4 + - name: FreeBSD 13.2 + test: freebsd/13.2 + groups: + - 1 + - 2 + - stage: Remote_2_15 + displayName: Remote 2.15 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + testFormat: 2.15/{0} + targets: + - name: RHEL 7.9 + test: rhel/7.9 + - name: FreeBSD 13.1 + test: freebsd/13.1 + groups: + - 1 + - 2 + - stage: Remote_2_14 + displayName: Remote 2.14 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + testFormat: 2.14/{0} + targets: + - name: macOS 12.0 + test: macos/12.0 + - name: RHEL 9.0 + test: rhel/9.0 + - name: FreeBSD 12.3 + test: freebsd/12.3 + groups: + - 1 + - 2 + - stage: Remote_2_13 + displayName: Remote 2.13 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + testFormat: 2.13/{0} + targets: + - name: RHEL 8.5 + test: rhel/8.5 + - name: FreeBSD 13.0 + test: freebsd/13.0 + groups: + - 1 + - 2 +### Generic + - stage: Generic_devel + displayName: Generic devel + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + nameFormat: Python {0} + testFormat: devel/generic/{0} + targets: + - test: 2.7 + - test: 3.6 + - test: 3.7 + # - test: 3.8 + # - test: 3.9 + # - test: "3.10" + - test: "3.11" + groups: + - 1 + - 2 + - stage: Generic_2_15 + displayName: Generic 2.15 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + nameFormat: Python {0} + testFormat: 2.15/generic/{0} + targets: + - test: 3.5 + - test: "3.10" + groups: + - 1 + - 2 + - stage: Generic_2_14 + displayName: Generic 2.14 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + nameFormat: Python {0} + testFormat: 2.14/generic/{0} + targets: + - test: 3.9 + groups: + - 1 + - 2 + - stage: Generic_2_13 + displayName: Generic 2.13 + dependsOn: [] + jobs: + - template: templates/matrix.yml + parameters: + nameFormat: Python {0} + testFormat: 2.13/generic/{0} + targets: + - test: 3.8 + groups: + - 1 + - 2 + + ## Finally + + - stage: Summary + condition: succeededOrFailed() + dependsOn: + - Ansible_devel + - Ansible_2_15 + - Ansible_2_14 + - Ansible_2_13 + - Remote_devel_extra_vms + - Remote_devel + - Remote_2_15 + - Remote_2_14 + - Remote_2_13 + - Docker_devel + - Docker_2_15 + - Docker_2_14 + - Docker_2_13 + - Docker_community_devel + - Generic_devel + - Generic_2_15 + - Generic_2_14 + - Generic_2_13 + jobs: + - template: templates/coverage.yml diff --git a/ansible_collections/community/crypto/.azure-pipelines/scripts/aggregate-coverage.sh b/ansible_collections/community/crypto/.azure-pipelines/scripts/aggregate-coverage.sh new file mode 100755 index 000000000..19f078f24 --- /dev/null +++ b/ansible_collections/community/crypto/.azure-pipelines/scripts/aggregate-coverage.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Aggregate code coverage results for later processing. + +set -o pipefail -eu + +agent_temp_directory="$1" + +PATH="${PWD}/bin:${PATH}" + +mkdir "${agent_temp_directory}/coverage/" + +if [[ "$(ansible --version)" =~ \ 2\.9\. ]]; then + exit +fi + +options=(--venv --venv-system-site-packages --color -v) + +ansible-test coverage combine --group-by command --export "${agent_temp_directory}/coverage/" "${options[@]}" + +if ansible-test coverage analyze targets generate --help >/dev/null 2>&1; then + # Only analyze coverage if the installed version of ansible-test supports it. + # Doing so allows this script to work unmodified for multiple Ansible versions. + ansible-test coverage analyze targets generate "${agent_temp_directory}/coverage/coverage-analyze-targets.json" "${options[@]}" +fi diff --git a/ansible_collections/community/crypto/.azure-pipelines/scripts/combine-coverage.py b/ansible_collections/community/crypto/.azure-pipelines/scripts/combine-coverage.py new file mode 100755 index 000000000..3b2fd993d --- /dev/null +++ b/ansible_collections/community/crypto/.azure-pipelines/scripts/combine-coverage.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +""" +Combine coverage data from multiple jobs, keeping the data only from the most recent attempt from each job. +Coverage artifacts must be named using the format: "Coverage $(System.JobAttempt) {StableUniqueNameForEachJob}" +The recommended coverage artifact name format is: Coverage $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName) +Keep in mind that Azure Pipelines does not enforce unique job display names (only names). +It is up to pipeline authors to avoid name collisions when deviating from the recommended format. +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import re +import shutil +import sys + + +def main(): + """Main program entry point.""" + source_directory = sys.argv[1] + + if '/ansible_collections/' in os.getcwd(): + output_path = "tests/output" + else: + output_path = "test/results" + + destination_directory = os.path.join(output_path, 'coverage') + + if not os.path.exists(destination_directory): + os.makedirs(destination_directory) + + jobs = {} + count = 0 + + for name in os.listdir(source_directory): + match = re.search('^Coverage (?P<attempt>[0-9]+) (?P<label>.+)$', name) + label = match.group('label') + attempt = int(match.group('attempt')) + jobs[label] = max(attempt, jobs.get(label, 0)) + + for label, attempt in jobs.items(): + name = 'Coverage {attempt} {label}'.format(label=label, attempt=attempt) + source = os.path.join(source_directory, name) + source_files = os.listdir(source) + + for source_file in source_files: + source_path = os.path.join(source, source_file) + destination_path = os.path.join(destination_directory, source_file + '.' + label) + print('"%s" -> "%s"' % (source_path, destination_path)) + shutil.copyfile(source_path, destination_path) + count += 1 + + print('Coverage file count: %d' % count) + print('##vso[task.setVariable variable=coverageFileCount]%d' % count) + print('##vso[task.setVariable variable=outputPath]%s' % output_path) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/.azure-pipelines/scripts/process-results.sh b/ansible_collections/community/crypto/.azure-pipelines/scripts/process-results.sh new file mode 100755 index 000000000..1f4b8e4f1 --- /dev/null +++ b/ansible_collections/community/crypto/.azure-pipelines/scripts/process-results.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Check the test results and set variables for use in later steps. + +set -o pipefail -eu + +if [[ "$PWD" =~ /ansible_collections/ ]]; then + output_path="tests/output" +else + output_path="test/results" +fi + +echo "##vso[task.setVariable variable=outputPath]${output_path}" + +if compgen -G "${output_path}"'/junit/*.xml' > /dev/null; then + echo "##vso[task.setVariable variable=haveTestResults]true" +fi + +if compgen -G "${output_path}"'/bot/ansible-test-*' > /dev/null; then + echo "##vso[task.setVariable variable=haveBotResults]true" +fi + +if compgen -G "${output_path}"'/coverage/*' > /dev/null; then + echo "##vso[task.setVariable variable=haveCoverageData]true" +fi diff --git a/ansible_collections/community/crypto/.azure-pipelines/scripts/publish-codecov.py b/ansible_collections/community/crypto/.azure-pipelines/scripts/publish-codecov.py new file mode 100755 index 000000000..58e32f6d3 --- /dev/null +++ b/ansible_collections/community/crypto/.azure-pipelines/scripts/publish-codecov.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +""" +Upload code coverage reports to codecov.io. +Multiple coverage files from multiple languages are accepted and aggregated after upload. +Python coverage, as well as PowerShell and Python stubs can all be uploaded. +""" + +import argparse +import dataclasses +import pathlib +import shutil +import subprocess +import tempfile +import typing as t +import urllib.request + + +@dataclasses.dataclass(frozen=True) +class CoverageFile: + name: str + path: pathlib.Path + flags: t.List[str] + + +@dataclasses.dataclass(frozen=True) +class Args: + dry_run: bool + path: pathlib.Path + + +def parse_args() -> Args: + parser = argparse.ArgumentParser() + parser.add_argument('-n', '--dry-run', action='store_true') + parser.add_argument('path', type=pathlib.Path) + + args = parser.parse_args() + + # Store arguments in a typed dataclass + fields = dataclasses.fields(Args) + kwargs = {field.name: getattr(args, field.name) for field in fields} + + return Args(**kwargs) + + +def process_files(directory: pathlib.Path) -> t.Tuple[CoverageFile, ...]: + processed = [] + for file in directory.joinpath('reports').glob('coverage*.xml'): + name = file.stem.replace('coverage=', '') + + # Get flags from name + flags = name.replace('-powershell', '').split('=') # Drop '-powershell' suffix + flags = [flag if not flag.startswith('stub') else flag.split('-')[0] for flag in flags] # Remove "-01" from stub files + + processed.append(CoverageFile(name, file, flags)) + + return tuple(processed) + + +def upload_files(codecov_bin: pathlib.Path, files: t.Tuple[CoverageFile, ...], dry_run: bool = False) -> None: + for file in files: + cmd = [ + str(codecov_bin), + '--name', file.name, + '--file', str(file.path), + ] + for flag in file.flags: + cmd.extend(['--flags', flag]) + + if dry_run: + print(f'DRY-RUN: Would run command: {cmd}') + continue + + subprocess.run(cmd, check=True) + + +def download_file(url: str, dest: pathlib.Path, flags: int, dry_run: bool = False) -> None: + if dry_run: + print(f'DRY-RUN: Would download {url} to {dest} and set mode to {flags:o}') + return + + with urllib.request.urlopen(url) as resp: + with dest.open('w+b') as f: + # Read data in chunks rather than all at once + shutil.copyfileobj(resp, f, 64 * 1024) + + dest.chmod(flags) + + +def main(): + args = parse_args() + url = 'https://ansible-ci-files.s3.amazonaws.com/codecov/linux/codecov' + with tempfile.TemporaryDirectory(prefix='codecov-') as tmpdir: + codecov_bin = pathlib.Path(tmpdir) / 'codecov' + download_file(url, codecov_bin, 0o755, args.dry_run) + + files = process_files(args.path) + upload_files(codecov_bin, files, args.dry_run) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/.azure-pipelines/scripts/report-coverage.sh b/ansible_collections/community/crypto/.azure-pipelines/scripts/report-coverage.sh new file mode 100755 index 000000000..63ac7f904 --- /dev/null +++ b/ansible_collections/community/crypto/.azure-pipelines/scripts/report-coverage.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Generate code coverage reports for uploading to Azure Pipelines and codecov.io. + +set -o pipefail -eu + +PATH="${PWD}/bin:${PATH}" + +if [[ "$(ansible --version)" =~ \ 2\.9\. ]]; then + exit +fi + +if ! ansible-test --help >/dev/null 2>&1; then + # Install the devel version of ansible-test for generating code coverage reports. + # This is only used by Ansible Collections, which are typically tested against multiple Ansible versions (in separate jobs). + # Since a version of ansible-test is required that can work the output from multiple older releases, the devel version is used. + pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check +fi + +ansible-test coverage xml --group-by command --stub --venv --venv-system-site-packages --color -v diff --git a/ansible_collections/community/crypto/.azure-pipelines/scripts/run-tests.sh b/ansible_collections/community/crypto/.azure-pipelines/scripts/run-tests.sh new file mode 100755 index 000000000..2cfdcf61e --- /dev/null +++ b/ansible_collections/community/crypto/.azure-pipelines/scripts/run-tests.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Configure the test environment and run the tests. + +set -o pipefail -eu + +entry_point="$1" +test="$2" +read -r -a coverage_branches <<< "$3" # space separated list of branches to run code coverage on for scheduled builds + +export COMMIT_MESSAGE +export COMPLETE +export COVERAGE +export IS_PULL_REQUEST + +if [ "${SYSTEM_PULLREQUEST_TARGETBRANCH:-}" ]; then + IS_PULL_REQUEST=true + COMMIT_MESSAGE=$(git log --format=%B -n 1 HEAD^2) +else + IS_PULL_REQUEST= + COMMIT_MESSAGE=$(git log --format=%B -n 1 HEAD) +fi + +COMPLETE= +COVERAGE= + +if [ "${BUILD_REASON}" = "Schedule" ]; then + COMPLETE=yes + + if printf '%s\n' "${coverage_branches[@]}" | grep -q "^${BUILD_SOURCEBRANCHNAME}$"; then + COVERAGE=yes + fi +fi + +"${entry_point}" "${test}" 2>&1 | "$(dirname "$0")/time-command.py" diff --git a/ansible_collections/community/crypto/.azure-pipelines/scripts/time-command.py b/ansible_collections/community/crypto/.azure-pipelines/scripts/time-command.py new file mode 100755 index 000000000..85a7c3c17 --- /dev/null +++ b/ansible_collections/community/crypto/.azure-pipelines/scripts/time-command.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Prepends a relative timestamp to each input line from stdin and writes it to stdout.""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import time + + +def main(): + """Main program entry point.""" + start = time.time() + + sys.stdin.reconfigure(errors='surrogateescape') + sys.stdout.reconfigure(errors='surrogateescape') + + for line in sys.stdin: + seconds = time.time() - start + sys.stdout.write('%02d:%02d %s' % (seconds // 60, seconds % 60, line)) + sys.stdout.flush() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/.azure-pipelines/templates/coverage.yml b/ansible_collections/community/crypto/.azure-pipelines/templates/coverage.yml new file mode 100644 index 000000000..3c8841aa2 --- /dev/null +++ b/ansible_collections/community/crypto/.azure-pipelines/templates/coverage.yml @@ -0,0 +1,44 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# This template adds a job for processing code coverage data. +# It will upload results to Azure Pipelines and codecov.io. +# Use it from a job stage that completes after all other jobs have completed. +# This can be done by placing it in a separate summary stage that runs after the test stage(s) have completed. + +jobs: + - job: Coverage + displayName: Code Coverage + container: default + workspace: + clean: all + steps: + - checkout: self + fetchDepth: $(fetchDepth) + path: $(checkoutPath) + - task: DownloadPipelineArtifact@2 + displayName: Download Coverage Data + inputs: + path: coverage/ + patterns: "Coverage */*=coverage.combined" + - bash: .azure-pipelines/scripts/combine-coverage.py coverage/ + displayName: Combine Coverage Data + - bash: .azure-pipelines/scripts/report-coverage.sh + displayName: Generate Coverage Report + condition: gt(variables.coverageFileCount, 0) + - task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: Cobertura + # Azure Pipelines only accepts a single coverage data file. + # That means only Python or PowerShell coverage can be uploaded, but not both. + # Set the "pipelinesCoverage" variable to determine which type is uploaded. + # Use "coverage" for Python and "coverage-powershell" for PowerShell. + summaryFileLocation: "$(outputPath)/reports/$(pipelinesCoverage).xml" + displayName: Publish to Azure Pipelines + condition: gt(variables.coverageFileCount, 0) + - bash: .azure-pipelines/scripts/publish-codecov.py "$(outputPath)" + displayName: Publish to codecov.io + condition: gt(variables.coverageFileCount, 0) + continueOnError: true diff --git a/ansible_collections/community/crypto/.azure-pipelines/templates/matrix.yml b/ansible_collections/community/crypto/.azure-pipelines/templates/matrix.yml new file mode 100644 index 000000000..487637585 --- /dev/null +++ b/ansible_collections/community/crypto/.azure-pipelines/templates/matrix.yml @@ -0,0 +1,60 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# This template uses the provided targets and optional groups to generate a matrix which is then passed to the test template. +# If this matrix template does not provide the required functionality, consider using the test template directly instead. + +parameters: + # A required list of dictionaries, one per test target. + # Each item in the list must contain a "test" or "name" key. + # Both may be provided. If one is omitted, the other will be used. + - name: targets + type: object + + # An optional list of values which will be used to multiply the targets list into a matrix. + # Values can be strings or numbers. + - name: groups + type: object + default: [] + + # An optional format string used to generate the job name. + # - {0} is the name of an item in the targets list. + - name: nameFormat + type: string + default: "{0}" + + # An optional format string used to generate the test name. + # - {0} is the name of an item in the targets list. + - name: testFormat + type: string + default: "{0}" + + # An optional format string used to add the group to the job name. + # {0} is the formatted name of an item in the targets list. + # {{1}} is the group -- be sure to include the double "{{" and "}}". + - name: nameGroupFormat + type: string + default: "{0} - {{1}}" + + # An optional format string used to add the group to the test name. + # {0} is the formatted test of an item in the targets list. + # {{1}} is the group -- be sure to include the double "{{" and "}}". + - name: testGroupFormat + type: string + default: "{0}/{{1}}" + +jobs: + - template: test.yml + parameters: + jobs: + - ${{ if eq(length(parameters.groups), 0) }}: + - ${{ each target in parameters.targets }}: + - name: ${{ format(parameters.nameFormat, coalesce(target.name, target.test)) }} + test: ${{ format(parameters.testFormat, coalesce(target.test, target.name)) }} + - ${{ if not(eq(length(parameters.groups), 0)) }}: + - ${{ each group in parameters.groups }}: + - ${{ each target in parameters.targets }}: + - name: ${{ format(format(parameters.nameGroupFormat, parameters.nameFormat), coalesce(target.name, target.test), group) }} + test: ${{ format(format(parameters.testGroupFormat, parameters.testFormat), coalesce(target.test, target.name), group) }} diff --git a/ansible_collections/community/crypto/.azure-pipelines/templates/test.yml b/ansible_collections/community/crypto/.azure-pipelines/templates/test.yml new file mode 100644 index 000000000..700cf629d --- /dev/null +++ b/ansible_collections/community/crypto/.azure-pipelines/templates/test.yml @@ -0,0 +1,50 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# This template uses the provided list of jobs to create test one or more test jobs. +# It can be used directly if needed, or through the matrix template. + +parameters: + # A required list of dictionaries, one per test job. + # Each item in the list must contain a "job" and "name" key. + - name: jobs + type: object + +jobs: + - ${{ each job in parameters.jobs }}: + - job: test_${{ replace(replace(replace(job.test, '/', '_'), '.', '_'), '-', '_') }} + displayName: ${{ job.name }} + container: default + workspace: + clean: all + steps: + - checkout: self + fetchDepth: $(fetchDepth) + path: $(checkoutPath) + - bash: .azure-pipelines/scripts/run-tests.sh "$(entryPoint)" "${{ job.test }}" "$(coverageBranches)" + displayName: Run Tests + - bash: .azure-pipelines/scripts/process-results.sh + condition: succeededOrFailed() + displayName: Process Results + - bash: .azure-pipelines/scripts/aggregate-coverage.sh "$(Agent.TempDirectory)" + condition: eq(variables.haveCoverageData, 'true') + displayName: Aggregate Coverage Data + - task: PublishTestResults@2 + condition: eq(variables.haveTestResults, 'true') + inputs: + testResultsFiles: "$(outputPath)/junit/*.xml" + displayName: Publish Test Results + - task: PublishPipelineArtifact@1 + condition: eq(variables.haveBotResults, 'true') + displayName: Publish Bot Results + inputs: + targetPath: "$(outputPath)/bot/" + artifactName: "Bot $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName)" + - task: PublishPipelineArtifact@1 + condition: eq(variables.haveCoverageData, 'true') + displayName: Publish Coverage Data + inputs: + targetPath: "$(Agent.TempDirectory)/coverage/" + artifactName: "Coverage $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName)" diff --git a/ansible_collections/community/crypto/.github/dependabot.yml b/ansible_collections/community/crypto/.github/dependabot.yml new file mode 100644 index 000000000..2f4ff900d --- /dev/null +++ b/ansible_collections/community/crypto/.github/dependabot.yml @@ -0,0 +1,11 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/ansible_collections/community/crypto/.github/patchback.yml b/ansible_collections/community/crypto/.github/patchback.yml new file mode 100644 index 000000000..5ee7812ed --- /dev/null +++ b/ansible_collections/community/crypto/.github/patchback.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +backport_branch_prefix: patchback/backports/ +backport_label_prefix: backport- +target_branch_prefix: stable- +... diff --git a/ansible_collections/community/crypto/.github/workflows/ansible-test.yml b/ansible_collections/community/crypto/.github/workflows/ansible-test.yml new file mode 100644 index 000000000..325dc5275 --- /dev/null +++ b/ansible_collections/community/crypto/.github/workflows/ansible-test.yml @@ -0,0 +1,227 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# For the comprehensive list of the inputs supported by the ansible-community/ansible-test-gh-action GitHub Action, see +# https://github.com/marketplace/actions/ansible-test + +name: EOL CI +on: + # Run EOL CI against all pushes (direct commits, also merged PRs), Pull Requests + push: + branches: + - main + - stable-* + pull_request: + # Run EOL CI once per day (at 09:00 UTC) + schedule: + - cron: '0 9 * * *' + +concurrency: + # Make sure there is at most one active run per PR, but do not cancel any non-PR runs + group: ${{ github.workflow }}-${{ (github.head_ref && github.event.number) || github.run_id }} + cancel-in-progress: true + +jobs: + sanity: + name: EOL Sanity (Ⓐ${{ matrix.ansible }}) + strategy: + matrix: + ansible: + - '2.9' + - '2.10' + - '2.11' + - '2.12' + # Ansible-test on various stable branches does not yet work well with cgroups v2. + # Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04 + # image for these stable branches. The list of branches where this is necessary will + # shrink over time, check out https://github.com/ansible-collections/news-for-maintainers/issues/28 + # for the latest list. + runs-on: >- + ${{ contains(fromJson( + '["2.9", "2.10", "2.11"]' + ), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }} + steps: + - name: Perform sanity testing + uses: felixfontein/ansible-test-gh-action@main + with: + ansible-core-github-repository-slug: ${{ contains(fromJson('["2.10", "2.11"]'), matrix.ansible) && 'felixfontein/ansible' || 'ansible/ansible' }} + ansible-core-version: stable-${{ matrix.ansible }} + coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }} + pull-request-change-detection: 'true' + testing-type: sanity + + units: + # Ansible-test on various stable branches does not yet work well with cgroups v2. + # Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04 + # image for these stable branches. The list of branches where this is necessary will + # shrink over time, check out https://github.com/ansible-collections/news-for-maintainers/issues/28 + # for the latest list. + runs-on: >- + ${{ contains(fromJson( + '["2.9", "2.10", "2.11"]' + ), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }} + name: EOL Units (Ⓐ${{ matrix.ansible }}) + strategy: + # As soon as the first unit test fails, cancel the others to free up the CI queue + fail-fast: true + matrix: + ansible: + - '2.9' + - '2.10' + - '2.11' + - '2.12' + + steps: + - name: >- + Perform unit testing against + Ansible version ${{ matrix.ansible }} + uses: felixfontein/ansible-test-gh-action@main + with: + ansible-core-github-repository-slug: ${{ contains(fromJson('["2.10", "2.11"]'), matrix.ansible) && 'felixfontein/ansible' || 'ansible/ansible' }} + ansible-core-version: stable-${{ matrix.ansible }} + coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }} + pull-request-change-detection: 'true' + testing-type: units + + integration: + # Ansible-test on various stable branches does not yet work well with cgroups v2. + # Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04 + # image for these stable branches. The list of branches where this is necessary will + # shrink over time, check out https://github.com/ansible-collections/news-for-maintainers/issues/28 + # for the latest list. + runs-on: >- + ${{ contains(fromJson( + '["2.9", "2.10", "2.11"]' + ), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }} + name: EOL I (Ⓐ${{ matrix.ansible }}+${{ matrix.docker }}+py${{ matrix.python }}:${{ matrix.target }}) + strategy: + fail-fast: false + matrix: + ansible: + - '' + docker: + - '' + python: + - '' + target: + - '' + exclude: + - ansible: '' + include: + # 2.9 + - ansible: '2.9' + docker: fedora31 + python: '' + target: azp/posix/1/ + - ansible: '2.9' + docker: fedora31 + python: '' + target: azp/posix/2/ + - ansible: '2.9' + docker: ubuntu1804 + python: '' + target: azp/posix/1/ + - ansible: '2.9' + docker: ubuntu1804 + python: '' + target: azp/posix/2/ + - ansible: '2.9' + docker: default + python: '2.7' + target: azp/generic/1/ + - ansible: '2.9' + docker: default + python: '2.7' + target: azp/generic/2/ + # 2.10 + - ansible: '2.10' + docker: centos6 + python: '' + target: azp/posix/1/ + - ansible: '2.10' + docker: centos6 + python: '' + target: azp/posix/2/ + - ansible: '2.10' + docker: default + python: '3.6' + target: azp/generic/1/ + - ansible: '2.10' + docker: default + python: '3.6' + target: azp/generic/2/ + # 2.11 + - ansible: '2.11' + docker: fedora32 + python: '' + target: azp/posix/1/ + - ansible: '2.11' + docker: fedora32 + python: '' + target: azp/posix/2/ + - ansible: '2.11' + docker: alpine3 + python: '' + target: azp/posix/1/ + - ansible: '2.11' + docker: alpine3 + python: '' + target: azp/posix/2/ + - ansible: '2.11' + docker: default + python: '3.8' + target: azp/generic/1/ + - ansible: '2.11' + docker: default + python: '3.8' + target: azp/generic/2/ + # 2.12 + - ansible: '2.12' + docker: centos6 + python: '' + target: azp/posix/1/ + - ansible: '2.12' + docker: centos6 + python: '' + target: azp/posix/2/ + - ansible: '2.12' + docker: fedora33 + python: '' + target: azp/posix/1/ + - ansible: '2.12' + docker: fedora33 + python: '' + target: azp/posix/2/ + - ansible: '2.12' + docker: default + python: '2.6' + target: azp/generic/1/ + - ansible: '2.12' + docker: default + python: '3.9' + target: azp/generic/2/ + + steps: + - name: >- + Perform integration testing against + Ansible version ${{ matrix.ansible }} + under Python ${{ matrix.python }} + uses: felixfontein/ansible-test-gh-action@main + with: + ansible-core-github-repository-slug: ${{ contains(fromJson('["2.10", "2.11"]'), matrix.ansible) && 'felixfontein/ansible' || 'ansible/ansible' }} + ansible-core-version: stable-${{ matrix.ansible }} + coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }} + docker-image: ${{ matrix.docker }} + integration-continue-on-error: 'false' + integration-diff: 'false' + integration-retry-on-error: 'true' + pre-test-cmd: >- + git clone --depth=1 --single-branch https://github.com/ansible-collections/community.internal_test_tools.git ../../community/internal_test_tools + ; + git clone --depth=1 --single-branch https://github.com/ansible-collections/community.general.git ../../community/general + pull-request-change-detection: 'true' + target: ${{ matrix.target }} + target-python-version: ${{ matrix.python }} + testing-type: integration diff --git a/ansible_collections/community/crypto/.github/workflows/docs-pr.yml b/ansible_collections/community/crypto/.github/workflows/docs-pr.yml new file mode 100644 index 000000000..e55c30a63 --- /dev/null +++ b/ansible_collections/community/crypto/.github/workflows/docs-pr.yml @@ -0,0 +1,92 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +name: Collection Docs +concurrency: + group: docs-pr-${{ github.head_ref }} + cancel-in-progress: true +on: + pull_request_target: + types: [opened, synchronize, reopened, closed] + +env: + GHP_BASE_URL: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }} + +jobs: + build-docs: + permissions: + contents: read + name: Build Ansible Docs + uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-pr.yml@main + with: + collection-name: community.crypto + init-lenient: false + init-fail-on-error: true + squash-hierarchy: true + init-project: Community.Crypto Collection + init-copyright: Community.Crypto Contributors + init-title: Community.Crypto Collection Documentation + init-html-short-title: Community.Crypto Collection Docs + init-extra-html-theme-options: | + documentation_home_url=https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/branch/main/ + render-file-line: '> * `$<status>` [$<path_tail>](https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/pr/${{ github.event.number }}/$<path_tail>)' + + publish-docs-gh-pages: + # for now we won't run this on forks + if: github.repository == 'ansible-collections/community.crypto' + permissions: + contents: write + needs: [build-docs] + name: Publish Ansible Docs + uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-publish-gh-pages.yml@main + with: + artifact-name: ${{ needs.build-docs.outputs.artifact-name }} + action: ${{ (github.event.action == 'closed' || needs.build-docs.outputs.changed != 'true') && 'teardown' || 'publish' }} + secrets: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + comment: + permissions: + pull-requests: write + runs-on: ubuntu-latest + needs: [build-docs, publish-docs-gh-pages] + name: PR comments + steps: + - name: PR comment + uses: ansible-community/github-docs-build/actions/ansible-docs-build-comment@main + with: + body-includes: '## Docs Build' + reactions: heart + action: ${{ needs.build-docs.outputs.changed != 'true' && 'remove' || '' }} + on-closed-body: | + ## Docs Build 📝 + + This PR is closed and any previously published docsite has been unpublished. + on-merged-body: | + ## Docs Build 📝 + + Thank you for contribution!✨ + + This PR has been merged and the docs are now incorporated into `main`: + ${{ env.GHP_BASE_URL }}/branch/main + body: | + ## Docs Build 📝 + + Thank you for contribution!✨ + + The docs for **this PR** have been published here: + ${{ env.GHP_BASE_URL }}/pr/${{ github.event.number }} + + You can compare to the docs for the `main` branch here: + ${{ env.GHP_BASE_URL }}/branch/main + + The docsite for **this PR** is also available for download as an artifact from this run: + ${{ needs.build-docs.outputs.artifact-url }} + + File changes: + + ${{ needs.build-docs.outputs.diff-files-rendered }} + + ${{ needs.build-docs.outputs.diff-rendered }} diff --git a/ansible_collections/community/crypto/.github/workflows/docs-push.yml b/ansible_collections/community/crypto/.github/workflows/docs-push.yml new file mode 100644 index 000000000..2c798c781 --- /dev/null +++ b/ansible_collections/community/crypto/.github/workflows/docs-push.yml @@ -0,0 +1,52 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +name: Collection Docs +concurrency: + group: docs-push-${{ github.sha }} + cancel-in-progress: true +on: + push: + branches: + - main + - stable-* + tags: + - '*' + # Run CI once per day (at 09:00 UTC) + schedule: + - cron: '0 9 * * *' + # Allow manual trigger (for newer antsibull-docs, sphinx-ansible-theme, ... versions) + workflow_dispatch: + +jobs: + build-docs: + permissions: + contents: read + name: Build Ansible Docs + uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-push.yml@main + with: + collection-name: community.crypto + init-lenient: false + init-fail-on-error: true + squash-hierarchy: true + init-project: Community.Crypto Collection + init-copyright: Community.Crypto Contributors + init-title: Community.Crypto Collection Documentation + init-html-short-title: Community.Crypto Collection Docs + init-extra-html-theme-options: | + documentation_home_url=https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/branch/main/ + + publish-docs-gh-pages: + # for now we won't run this on forks + if: github.repository == 'ansible-collections/community.crypto' + permissions: + contents: write + needs: [build-docs] + name: Publish Ansible Docs + uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-publish-gh-pages.yml@main + with: + artifact-name: ${{ needs.build-docs.outputs.artifact-name }} + secrets: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/ansible_collections/community/crypto/.github/workflows/ee.yml b/ansible_collections/community/crypto/.github/workflows/ee.yml new file mode 100644 index 000000000..edd4d047b --- /dev/null +++ b/ansible_collections/community/crypto/.github/workflows/ee.yml @@ -0,0 +1,185 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +name: execution environment +on: + # Run CI against all pushes (direct commits, also merged PRs), Pull Requests + push: + branches: + - main + - stable-* + pull_request: + # Run CI once per day (at 04:45 UTC) + # This ensures that even if there haven't been commits that we are still testing against latest version of ansible-builder + schedule: + - cron: '45 4 * * *' + +env: + NAMESPACE: community + COLLECTION_NAME: crypto + +jobs: + build: + name: Build and test EE (${{ matrix.name }}) + strategy: + fail-fast: false + matrix: + name: + - '' + ansible_core: + - '' + ansible_runner: + - '' + base_image: + - '' + pre_base: + - '' + extra_vars: + - '' + other_deps: + - '' + exclude: + - ansible_core: '' + include: + - name: ansible-core devel @ RHEL UBI 9 + ansible_core: https://github.com/ansible/ansible/archive/devel.tar.gz + ansible_runner: ansible-runner + base_image: docker.io/redhat/ubi9:latest + pre_base: '"#"' + # For some reason ansible-builder will not install EPEL dependencies on RHEL + extra_vars: -e has_no_pyopenssl=true + - name: ansible-core 2.15 @ Rocky Linux 9 + ansible_core: https://github.com/ansible/ansible/archive/stable-2.15.tar.gz + ansible_runner: ansible-runner + base_image: quay.io/rockylinux/rockylinux:9 + pre_base: RUN dnf install -y epel-release + # For some reason ansible-builder will not install EPEL dependencies on Rocky Linux + extra_vars: -e has_no_pyopenssl=true + - name: ansible-core 2.14 @ CentOS Stream 9 + ansible_core: https://github.com/ansible/ansible/archive/stable-2.14.tar.gz + ansible_runner: ansible-runner + base_image: quay.io/centos/centos:stream9 + pre_base: RUN dnf install -y epel-release epel-next-release + # For some reason, PyOpenSSL is **broken** on CentOS Stream 9 / EPEL + extra_vars: -e has_no_pyopenssl=true + - name: ansible-core 2.13 @ RHEL UBI 8 + ansible_core: https://github.com/ansible/ansible/archive/stable-2.13.tar.gz + ansible_runner: ansible-runner + other_deps: |2 + python_interpreter: + package_system: python39 python39-pip python39-wheel python39-cryptography + base_image: docker.io/redhat/ubi8:latest + pre_base: '"#"' + # We don't have PyOpenSSL for Python 3.9 + extra_vars: -e has_no_pyopenssl=true + - name: ansible-core 2.12 @ CentOS Stream 8 + ansible_core: https://github.com/ansible/ansible/archive/stable-2.12.tar.gz + ansible_runner: ansible-runner + other_deps: |2 + python_interpreter: + package_system: python39 python39-pip python39-wheel python39-cryptography + base_image: quay.io/centos/centos:stream8 + pre_base: '"#"' + # We don't have PyOpenSSL for Python 3.9 + extra_vars: -e has_no_pyopenssl=true + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v3 + with: + path: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install ansible-builder and ansible-navigator + run: pip install ansible-builder ansible-navigator + + - name: Verify requirements + run: ansible-builder introspect --sanitize . + + - name: Make sure galaxy.yml has version entry + run: >- + python -c + 'import yaml ; + f = open("galaxy.yml", "rb") ; + data = yaml.safe_load(f) ; + f.close() ; + data["version"] = data.get("version") or "0.0.1" ; + f = open("galaxy.yml", "wb") ; + f.write(yaml.dump(data).encode("utf-8")) ; + f.close() ; + ' + working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }} + + - name: Build collection + run: | + ansible-galaxy collection build --output-path ../../../ + working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }} + + - name: Create files for building execution environment + run: | + COLLECTION_FILENAME="$(ls "${{ env.NAMESPACE }}-${{ env.COLLECTION_NAME }}"-*.tar.gz)" + + # EE config + cat > execution-environment.yml <<EOF + --- + version: 3 + dependencies: + ansible_core: + package_pip: ${{ matrix.ansible_core }} + ansible_runner: + package_pip: ${{ matrix.ansible_runner }} + galaxy: requirements.yml + ${{ matrix.other_deps }} + + images: + base_image: + name: ${{ matrix.base_image }} + + additional_build_files: + - src: ${COLLECTION_FILENAME} + dest: src + + additional_build_steps: + prepend_base: + - ${{ matrix.pre_base }} + EOF + echo "::group::execution-environment.yml" + cat execution-environment.yml + echo "::endgroup::" + + # Requirements + cat > requirements.yml <<EOF + --- + collections: + - name: src/${COLLECTION_FILENAME} + type: file + EOF + echo "::group::requirements.yml" + cat requirements.yml + echo "::endgroup::" + + - name: Build image based on ${{ matrix.base_image }} + run: | + ansible-builder build --verbosity 3 --tag test-ee:latest --container-runtime podman + + - name: Show images + run: podman image ls + + - name: Run basic tests + run: > + ansible-navigator run + --mode stdout + --container-engine podman + --pull-policy never + --set-environment-variable ANSIBLE_PRIVATE_ROLE_VARS=true + --execution-environment-image test-ee:latest + -v + all.yml + ${{ matrix.extra_vars }} + working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}/tests/ee diff --git a/ansible_collections/community/crypto/.github/workflows/reuse.yml b/ansible_collections/community/crypto/.github/workflows/reuse.yml new file mode 100644 index 000000000..3b01cd8ac --- /dev/null +++ b/ansible_collections/community/crypto/.github/workflows/reuse.yml @@ -0,0 +1,34 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +name: Verify REUSE + +on: + push: + branches: [main] + pull_request: + branches: [main] + # Run CI once per day (at 04:45 UTC) + schedule: + - cron: '45 4 * * *' + +jobs: + check: + permissions: + contents: read + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + run: | + pip install reuse + + - name: Check REUSE compliance (except some PEM files) + run: | + rm -f tests/integration/targets/*/files/*.pem + rm -f tests/integration/targets/*/files/roots/*.pem + reuse lint diff --git a/ansible_collections/community/crypto/.reuse/dep5 b/ansible_collections/community/crypto/.reuse/dep5 new file mode 100644 index 000000000..0c3745ebf --- /dev/null +++ b/ansible_collections/community/crypto/.reuse/dep5 @@ -0,0 +1,5 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: changelogs/fragments/* +Copyright: Ansible Project +License: GPL-3.0-or-later diff --git a/ansible_collections/community/crypto/CHANGELOG.rst b/ansible_collections/community/crypto/CHANGELOG.rst new file mode 100644 index 000000000..940ed0c43 --- /dev/null +++ b/ansible_collections/community/crypto/CHANGELOG.rst @@ -0,0 +1,966 @@ +============================== +Community Crypto Release Notes +============================== + +.. contents:: Topics + + +v2.14.0 +======= + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- acme_certificate - allow to use no challenge by providing ``no challenge`` for the ``challenge`` option. This is needed for ACME servers where validation is done without challenges (https://github.com/ansible-collections/community.crypto/issues/613, https://github.com/ansible-collections/community.crypto/pull/615). +- acme_certificate - validate and wait for challenges in parallel instead handling them one after another (https://github.com/ansible-collections/community.crypto/pull/617). +- x509_certificate_info - added support for certificates in DER format when using ``path`` parameter (https://github.com/ansible-collections/community.crypto/issues/603). + +v2.13.1 +======= + +Release Summary +--------------- + +Bugfix release. + +Bugfixes +-------- + +- execution environment definition - fix installation of ``python3-pyOpenSSL`` package on CentOS and RHEL (https://github.com/ansible-collections/community.crypto/pull/606). +- execution environment definition - fix source of ``python3-pyOpenSSL`` package for Rocky Linux 9+ (https://github.com/ansible-collections/community.crypto/pull/606). + +v2.13.0 +======= + +Release Summary +--------------- + +Bugfix and maintenance release. + +Minor Changes +------------- + +- x509_crl - the ``crl_mode`` option has been added to replace the existing ``mode`` option (https://github.com/ansible-collections/community.crypto/issues/596). + +Deprecated Features +------------------- + +- x509_crl - the ``mode`` option is deprecated; use ``crl_mode`` instead. The ``mode`` option will change its meaning in community.crypto 3.0.0, and will refer to the CRL file's mode instead (https://github.com/ansible-collections/community.crypto/issues/596). + +Bugfixes +-------- + +- openssh_keypair - always generate a new key pair if the private key does not exist. Previously, the module would fail when ``regenerate=fail`` without an existing key, contradicting the documentation (https://github.com/ansible-collections/community.crypto/pull/598). +- x509_crl - remove problem with ansible-core 2.16 due to ``AnsibleModule`` is now validating the ``mode`` parameter's values (https://github.com/ansible-collections/community.crypto/issues/596). + +v2.12.0 +======= + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- get_certificate - add ``asn1_base64`` option to control whether the ASN.1 included in the ``extensions`` return value is binary data or Base64 encoded (https://github.com/ansible-collections/community.crypto/pull/592). + +v2.11.1 +======= + +Release Summary +--------------- + +Maintenance release with improved documentation. + +v2.11.0 +======= + +Release Summary +--------------- + +Feature and bugfix release. + +Minor Changes +------------- + +- get_certificate - adds ``ciphers`` option for custom cipher selection (https://github.com/ansible-collections/community.crypto/pull/571). + +Bugfixes +-------- + +- action plugin helper - fix handling of deprecations for ansible-core 2.14.2 (https://github.com/ansible-collections/community.crypto/pull/572). +- execution environment binary dependencies (bindep.txt) - fix ``python3-pyOpenSSL`` dependency resolution on RHEL 9+ / CentOS Stream 9+ platforms (https://github.com/ansible-collections/community.crypto/pull/575). +- various plugins - remove unnecessary imports (https://github.com/ansible-collections/community.crypto/pull/569). + +v2.10.0 +======= + +Release Summary +--------------- + +Bugfix and feature release. + +Bugfixes +-------- + +- openssl_csr, openssl_csr_pipe - prevent invalid values for ``crl_distribution_points`` that do not have one of ``full_name``, ``relative_name``, and ``crl_issuer`` (https://github.com/ansible-collections/community.crypto/pull/560). +- openssl_publickey_info - do not crash with internal error when public key cannot be parsed (https://github.com/ansible-collections/community.crypto/pull/551). + +New Plugins +----------- + +Filter +~~~~~~ + +- openssl_csr_info - Retrieve information from OpenSSL Certificate Signing Requests (CSR) +- openssl_privatekey_info - Retrieve information from OpenSSL private keys +- openssl_publickey_info - Retrieve information from OpenSSL public keys in PEM format +- split_pem - Split PEM file contents into multiple objects +- x509_certificate_info - Retrieve information from X.509 certificates in PEM format +- x509_crl_info - Retrieve information from X.509 CRLs in PEM format + +v2.9.0 +====== + +Release Summary +--------------- + +Regular feature release. + +Minor Changes +------------- + +- x509_certificate_info - adds ``issuer_uri`` field in return value based on Authority Information Access data (https://github.com/ansible-collections/community.crypto/pull/530). + +v2.8.1 +====== + +Release Summary +--------------- + +Maintenance release with improved documentation. + +v2.8.0 +====== + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- acme_* modules - handle more gracefully if CA's new nonce call does not return a nonce (https://github.com/ansible-collections/community.crypto/pull/525). +- acme_* modules - include symbolic HTTP status codes in error and log messages when available (https://github.com/ansible-collections/community.crypto/pull/524). +- openssl_pkcs12 - add option ``encryption_level`` which allows to chose ``compatibility2022`` when cryptography >= 38.0.0 is used to enable a more backwards compatible encryption algorithm. If cryptography uses OpenSSL 3.0.0 or newer, the default algorithm is not compatible with older software (https://github.com/ansible-collections/community.crypto/pull/523). + +v2.7.1 +====== + +Release Summary +--------------- + +Maintenance release. + +Bugfixes +-------- + +- acme_* modules - improve feedback when importing ``cryptography`` does not work (https://github.com/ansible-collections/community.crypto/issues/518, https://github.com/ansible-collections/community.crypto/pull/519). + +v2.7.0 +====== + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- acme* modules - also support the HTTP 503 Service Unavailable and 408 Request Timeout response status for automatic retries (https://github.com/ansible-collections/community.crypto/pull/513). + +Bugfixes +-------- + +- openssl_privatekey_pipe - ensure compatibility with newer versions of ansible-core (https://github.com/ansible-collections/community.crypto/pull/515). + +v2.6.0 +====== + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- acme* modules - support the HTTP 429 Too Many Requests response status (https://github.com/ansible-collections/community.crypto/pull/508). +- openssh_keypair - added ``pkcs1``, ``pkcs8``, and ``ssh`` to the available choices for the ``private_key_format`` option (https://github.com/ansible-collections/community.crypto/pull/511). + +v2.5.0 +====== + +Release Summary +--------------- + +Maintenance release with improved licensing declaration and documentation fixes. + +Minor Changes +------------- + +- All software licenses are now in the ``LICENSES/`` directory of the collection root. Moreover, ``SPDX-License-Identifier:`` is used to declare the applicable license for every file that is not automatically generated (https://github.com/ansible-collections/community.crypto/pull/491). + +v2.4.0 +====== + +Release Summary +--------------- + +Deprecation and bugfix release. No new features this time. + +Deprecated Features +------------------- + +- Support for Ansible 2.9 and ansible-base 2.10 is deprecated, and will be removed in the next major release (community.crypto 3.0.0). Some modules might still work with these versions afterwards, but we will no longer keep compatibility code that was needed to support them (https://github.com/ansible-collections/community.crypto/pull/460). + +Bugfixes +-------- + +- openssl_pkcs12 - when using the pyOpenSSL backend, do not crash when trying to read non-existing other certificates (https://github.com/ansible-collections/community.crypto/issues/486, https://github.com/ansible-collections/community.crypto/pull/487). + +v2.3.4 +====== + +Release Summary +--------------- + +Re-release of what was intended to be 2.3.3. + +A mistake during the release process caused the 2.3.3 tag to end up on the +commit for 1.9.17, which caused the release pipeline to re-publish 1.9.17 +as 2.3.3. + +This release is identical to what should have been 2.3.3, except that the +version number has been bumped to 2.3.4 and this changelog entry for 2.3.4 +has been added. + + +v2.3.3 +====== + +Release Summary +--------------- + +Bugfix release. + +Bugfixes +-------- + +- Include ``Apache-2.0.txt`` file for ``plugins/module_utils/crypto/_obj2txt.py`` and ``plugins/module_utils/crypto/_objects_data.py``. +- openssl_csr - the module no longer crashes with 'permitted_subtrees/excluded_subtrees must be a non-empty list or None' if only one of ``name_constraints_permitted`` and ``name_constraints_excluded`` is provided (https://github.com/ansible-collections/community.crypto/issues/481). +- x509_crl - do not crash when signing CRL with Ed25519 or Ed448 keys (https://github.com/ansible-collections/community.crypto/issues/473, https://github.com/ansible-collections/community.crypto/pull/474). + +v2.3.2 +====== + +Release Summary +--------------- + +Maintenance and bugfix release. + +Bugfixes +-------- + +- Include ``simplified_bsd.txt`` license file for the ECS module utils. +- certificate_complete_chain - do not stop execution if an unsupported signature algorithm is encountered; warn instead (https://github.com/ansible-collections/community.crypto/pull/457). + +v2.3.1 +====== + +Release Summary +--------------- + +Maintenance release. + +Bugfixes +-------- + +- Include ``PSF-license.txt`` file for ``plugins/module_utils/_version.py``. + +v2.3.0 +====== + +Release Summary +--------------- + +Feature and bugfix release. + +Minor Changes +------------- + +- Prepare collection for inclusion in an Execution Environment by declaring its dependencies. Please note that system packages are used for cryptography and PyOpenSSL, which can be rather limited. If you need features from newer cryptography versions, you will have to manually force a newer version to be installed by pip by specifying something like ``cryptography >= 37.0.0`` in your Execution Environment's Python dependencies file (https://github.com/ansible-collections/community.crypto/pull/440). +- Support automatic conversion for Internalionalized Domain Names (IDNs). When passing general names, for example Subject Altenative Names to ``community.crypto.openssl_csr``, these will automatically be converted to IDNA. Conversion will be done per label to IDNA2008 if possible, and IDNA2003 if IDNA2008 conversion fails for that label. Note that IDNA conversion requires `the Python idna library <https://pypi.org/project/idna/>`_ to be installed. Please note that depending on which versions of the cryptography library are used, it could try to process the converted IDNA another time with the Python ``idna`` library and reject IDNA2003 encoded values. Using a new enough ``cryptography`` version avoids this (https://github.com/ansible-collections/community.crypto/issues/426, https://github.com/ansible-collections/community.crypto/pull/436). +- acme_* modules - add parameter ``request_timeout`` to manage HTTP(S) request timeout (https://github.com/ansible-collections/community.crypto/issues/447, https://github.com/ansible-collections/community.crypto/pull/448). +- luks_devices - added ``perf_same_cpu_crypt``, ``perf_submit_from_crypt_cpus``, ``perf_no_read_workqueue``, ``perf_no_write_workqueue`` for performance tuning when opening LUKS2 containers (https://github.com/ansible-collections/community.crypto/issues/427). +- luks_devices - added ``persistent`` option when opening LUKS2 containers (https://github.com/ansible-collections/community.crypto/pull/434). +- openssl_csr_info - add ``name_encoding`` option to control the encoding (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436). +- openssl_pkcs12 - allow to provide the private key as text instead of having to read it from a file. This allows to store the private key in an encrypted form, for example in Ansible Vault (https://github.com/ansible-collections/community.crypto/pull/452). +- x509_certificate_info - add ``name_encoding`` option to control the encoding (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436). +- x509_crl - add ``name_encoding`` option to control the encoding (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436). +- x509_crl_info - add ``name_encoding`` option to control the encoding (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436). + +Bugfixes +-------- + +- Make collection more robust when PyOpenSSL is used with an incompatible cryptography version (https://github.com/ansible-collections/community.crypto/pull/445). +- x509_crl - fix crash when ``issuer`` for a revoked certificate is specified (https://github.com/ansible-collections/community.crypto/pull/441). + +v2.2.4 +====== + +Release Summary +--------------- + +Regular maintenance release. + +Bugfixes +-------- + +- openssh_* modules - fix exception handling to report traceback to users for enhanced traceability (https://github.com/ansible-collections/community.crypto/pull/417). + +v2.2.3 +====== + +Release Summary +--------------- + +Regular bugfix release. + +Bugfixes +-------- + +- luks_device - fix parsing of ``lsblk`` output when device name ends with ``crypt`` (https://github.com/ansible-collections/community.crypto/issues/409, https://github.com/ansible-collections/community.crypto/pull/410). + +v2.2.2 +====== + +Release Summary +--------------- + +Regular bugfix release. + +In this release, we extended the test matrix to include Alpine 3, ArchLinux, Debian Bullseye, and CentOS Stream 8. CentOS 8 was removed from the test matrix. + + +Bugfixes +-------- + +- certificate_complete_chain - allow multiple potential intermediate certificates to have the same subject (https://github.com/ansible-collections/community.crypto/issues/399, https://github.com/ansible-collections/community.crypto/pull/403). +- x509_certificate - for the ``ownca`` provider, check whether the CA private key actually belongs to the CA certificate (https://github.com/ansible-collections/community.crypto/pull/407). +- x509_certificate - regenerate certificate when the CA's public key changes for ``provider=ownca`` (https://github.com/ansible-collections/community.crypto/pull/407). +- x509_certificate - regenerate certificate when the CA's subject changes for ``provider=ownca`` (https://github.com/ansible-collections/community.crypto/issues/400, https://github.com/ansible-collections/community.crypto/pull/402). +- x509_certificate - regenerate certificate when the private key changes for ``provider=selfsigned`` (https://github.com/ansible-collections/community.crypto/pull/407). + +v2.2.1 +====== + +Release Summary +--------------- + +Bugfix release. + +Bugfixes +-------- + +- openssh_cert - fixed false ``changed`` status for ``host`` certificates when using ``full_idempotence`` (https://github.com/ansible-collections/community.crypto/issues/395, https://github.com/ansible-collections/community.crypto/pull/396). + +v2.2.0 +====== + +Release Summary +--------------- + +Regular bugfix and feature release. + +Minor Changes +------------- + +- openssh_cert - added ``ignore_timestamps`` parameter so it can be used semi-idempotent with relative timestamps in ``valid_to``/``valid_from`` (https://github.com/ansible-collections/community.crypto/issues/379). + +Bugfixes +-------- + +- luks_devices - set ``LANG`` and similar environment variables to avoid translated output, which can break some of the module's functionality like key management (https://github.com/ansible-collections/community.crypto/pull/388, https://github.com/ansible-collections/community.crypto/issues/385). + +v2.1.0 +====== + +Release Summary +--------------- + +Feature and bugfix release. + +Minor Changes +------------- + +- Adjust error messages that indicate ``cryptography`` is not installed from ``Can't`` to ``Cannot`` (https://github.com/ansible-collections/community.crypto/pull/374). + +Bugfixes +-------- + +- Various modules and plugins - use vendored version of ``distutils.version`` instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.crypto/pull/353). +- certificate_complete_chain - do not append root twice if the chain already ends with a root certificate (https://github.com/ansible-collections/community.crypto/pull/360). +- certificate_complete_chain - do not hang when infinite loop is found (https://github.com/ansible-collections/community.crypto/issues/355, https://github.com/ansible-collections/community.crypto/pull/360). + +New Modules +----------- + +- crypto_info - Retrieve cryptographic capabilities +- openssl_privatekey_convert - Convert OpenSSL private keys + +v2.0.2 +====== + +Release Summary +--------------- + +Documentation fix release. No actual code changes. + +v2.0.1 +====== + +Release Summary +--------------- + +Bugfix release with extra forward compatibility for newer versions of cryptography. + +Minor Changes +------------- + +- acme_* modules - fix usage of ``fetch_url`` with changes in latest ansible-core ``devel`` branch (https://github.com/ansible-collections/community.crypto/pull/339). + +Bugfixes +-------- + +- acme_certificate - avoid passing multiple certificates to ``cryptography``'s X.509 certificate loader when ``fullchain_dest`` is used (https://github.com/ansible-collections/community.crypto/pull/324). +- get_certificate, openssl_csr_info, x509_certificate_info - add fallback code for extension parsing that works with cryptography 36.0.0 and newer. This code re-serializes de-serialized extensions and thus can return slightly different values if the extension in the original CSR resp. certificate was not canonicalized correctly. This code is currently used as a fallback if the existing code stops working, but we will switch it to be the main code in a future release (https://github.com/ansible-collections/community.crypto/pull/331). +- luks_device - now also runs a built-in LUKS signature cleaner on ``state=absent`` to make sure that also the secondary LUKS2 header is wiped when older versions of wipefs are used (https://github.com/ansible-collections/community.crypto/issues/326, https://github.com/ansible-collections/community.crypto/pull/327). +- openssl_pkcs12 - use new PKCS#12 deserialization infrastructure from cryptography 36.0.0 if available (https://github.com/ansible-collections/community.crypto/pull/302). + +v2.0.0 +====== + +Release Summary +--------------- + +A new major release of the ``community.crypto`` collection. The main changes are removal of the PyOpenSSL backends for almost all modules (``openssl_pkcs12`` being the only exception), and removal of the ``assertonly`` provider in the ``x509_certificate`` provider. There are also some other breaking changes which should improve the user interface/experience of this collection long-term. + + +Minor Changes +------------- + +- acme_certificate - the ``subject`` and ``issuer`` fields in in the ``select_chain`` entries are now more strictly validated (https://github.com/ansible-collections/community.crypto/pull/316). +- openssl_csr, openssl_csr_pipe - provide a new ``subject_ordered`` option if the order of the components in the subject is of importance (https://github.com/ansible-collections/community.crypto/issues/291, https://github.com/ansible-collections/community.crypto/pull/316). +- openssl_csr, openssl_csr_pipe - there is now stricter validation of the values of the ``subject`` option (https://github.com/ansible-collections/community.crypto/pull/316). +- openssl_privatekey_info - add ``check_consistency`` option to request private key consistency checks to be done (https://github.com/ansible-collections/community.crypto/pull/309). +- x509_certificate, x509_certificate_pipe - add ``ignore_timestamps`` option which allows to enable idempotency for 'not before' and 'not after' options (https://github.com/ansible-collections/community.crypto/issues/295, https://github.com/ansible-collections/community.crypto/pull/317). +- x509_crl - provide a new ``issuer_ordered`` option if the order of the components in the issuer is of importance (https://github.com/ansible-collections/community.crypto/issues/291, https://github.com/ansible-collections/community.crypto/pull/316). +- x509_crl - there is now stricter validation of the values of the ``issuer`` option (https://github.com/ansible-collections/community.crypto/pull/316). + +Breaking Changes / Porting Guide +-------------------------------- + +- Adjust ``dirName`` text parsing and to text converting code to conform to `Sections 2 and 3 of RFC 4514 <https://datatracker.ietf.org/doc/html/rfc4514.html>`_. This is similar to how `cryptography handles this <https://cryptography.io/en/latest/x509/reference/#cryptography.x509.Name.rfc4514_string>`_ (https://github.com/ansible-collections/community.crypto/pull/274). +- acme module utils - removing compatibility code (https://github.com/ansible-collections/community.crypto/pull/290). +- acme_* modules - removed vendored copy of the Python library ``ipaddress``. If you are using Python 2.x, please make sure to install the library (https://github.com/ansible-collections/community.crypto/pull/287). +- compatibility module_utils - removed vendored copy of the Python library ``ipaddress`` (https://github.com/ansible-collections/community.crypto/pull/287). +- crypto module utils - removing compatibility code (https://github.com/ansible-collections/community.crypto/pull/290). +- get_certificate, openssl_csr_info, x509_certificate_info - depending on the ``cryptography`` version used, the modules might not return the ASN.1 value for an extension as contained in the certificate respectively CSR, but a re-encoded version of it. This should usually be identical to the value contained in the source file, unless the value was malformed. For extensions not handled by C(cryptography) the value contained in the source file is always returned unaltered (https://github.com/ansible-collections/community.crypto/pull/318). +- module_utils - removed various PyOpenSSL support functions and default backend values that are not needed for the openssl_pkcs12 module (https://github.com/ansible-collections/community.crypto/pull/273). +- openssl_csr, openssl_csr_pipe, x509_crl - the ``subject`` respectively ``issuer`` fields no longer ignore empty values, but instead fail when encountering them (https://github.com/ansible-collections/community.crypto/pull/316). +- openssl_privatekey_info - by default consistency checks are not run; they need to be explicitly requested by passing ``check_consistency=true`` (https://github.com/ansible-collections/community.crypto/pull/309). +- x509_crl - for idempotency checks, the ``issuer`` order is ignored. If order is important, use the new ``issuer_ordered`` option (https://github.com/ansible-collections/community.crypto/pull/316). + +Deprecated Features +------------------- + +- acme_* modules - ACME version 1 is now deprecated and support for it will be removed in community.crypto 2.0.0 (https://github.com/ansible-collections/community.crypto/pull/288). + +Removed Features (previously deprecated) +---------------------------------------- + +- acme_* modules - the ``acme_directory`` option is now required (https://github.com/ansible-collections/community.crypto/pull/290). +- acme_* modules - the ``acme_version`` option is now required (https://github.com/ansible-collections/community.crypto/pull/290). +- acme_account_facts - the deprecated redirect has been removed. Use community.crypto.acme_account_info instead (https://github.com/ansible-collections/community.crypto/pull/290). +- acme_account_info - ``retrieve_orders=url_list`` no longer returns the return value ``orders``. Use the ``order_uris`` return value instead (https://github.com/ansible-collections/community.crypto/pull/290). +- crypto.info module utils - the deprecated redirect has been removed. Use ``crypto.pem`` instead (https://github.com/ansible-collections/community.crypto/pull/290). +- get_certificate - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- openssl_certificate - the deprecated redirect has been removed. Use community.crypto.x509_certificate instead (https://github.com/ansible-collections/community.crypto/pull/290). +- openssl_certificate_info - the deprecated redirect has been removed. Use community.crypto.x509_certificate_info instead (https://github.com/ansible-collections/community.crypto/pull/290). +- openssl_csr - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- openssl_csr and openssl_csr_pipe - ``version`` now only accepts the (default) value 1 (https://github.com/ansible-collections/community.crypto/pull/290). +- openssl_csr_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- openssl_csr_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- openssl_privatekey - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- openssl_privatekey_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- openssl_privatekey_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- openssl_publickey - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- openssl_publickey_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- openssl_signature - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- openssl_signature_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- x509_certificate - remove ``assertonly`` provider (https://github.com/ansible-collections/community.crypto/pull/289). +- x509_certificate - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- x509_certificate_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). +- x509_certificate_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + +Bugfixes +-------- + +- cryptography backend - improve Unicode handling for Python 2 (https://github.com/ansible-collections/community.crypto/pull/313). +- get_certificate - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294). +- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294). +- openssl_pkcs12 - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/296). +- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294). + +v1.9.4 +====== + +Release Summary +--------------- + +Regular bugfix release. + +Bugfixes +-------- + +- acme_* modules - fix commands composed for OpenSSL backend to retrieve information on CSRs and certificates from stdin to use ``/dev/stdin`` instead of ``-``. This is needed for OpenSSL 1.0.1 and 1.0.2, apparently (https://github.com/ansible-collections/community.crypto/pull/279). +- acme_challenge_cert_helper - only return exception when cryptography is not installed, not when a too old version of it is installed. This prevents Ansible's callback to crash (https://github.com/ansible-collections/community.crypto/pull/281). + +v1.9.3 +====== + +Release Summary +--------------- + +Regular bugfix release. + +Bugfixes +-------- + +- openssl_csr and openssl_csr_pipe - make sure that Unicode strings are used to compare strings with the cryptography backend. This fixes idempotency problems with non-ASCII letters on Python 2 (https://github.com/ansible-collections/community.crypto/issues/270, https://github.com/ansible-collections/community.crypto/pull/271). + +v1.9.2 +====== + +Release Summary +--------------- + +Bugfix release to fix the changelog. No other change compared to 1.9.0. + +v1.9.1 +====== + +Release Summary +--------------- + +Accidental 1.9.1 release. Identical to 1.9.0. + +v1.9.0 +====== + +Release Summary +--------------- + +Regular feature release. + +Minor Changes +------------- + +- get_certificate - added ``starttls`` option to retrieve certificates from servers which require clients to request an encrypted connection (https://github.com/ansible-collections/community.crypto/pull/264). +- openssh_keypair - added ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/260). + +Bugfixes +-------- + +- keypair_backend module utils - simplify code to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263). +- openssh_keypair - fixed ``cryptography`` backend to preserve original file permissions when regenerating a keypair requires existing files to be overwritten (https://github.com/ansible-collections/community.crypto/pull/260). +- openssh_keypair - fixed error handling to restore original keypair if regeneration fails (https://github.com/ansible-collections/community.crypto/pull/260). +- x509_crl - restore inherited function signature to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263). + +v1.8.0 +====== + +Release Summary +--------------- + +Regular bugfix and feature release. + +Minor Changes +------------- + +- Avoid internal ansible-core module_utils in favor of equivalent public API available since at least Ansible 2.9 (https://github.com/ansible-collections/community.crypto/pull/253). +- openssh certificate module utils - new module_utils for parsing OpenSSH certificates (https://github.com/ansible-collections/community.crypto/pull/246). +- openssh_cert - added ``regenerate`` option to validate additional certificate parameters which trigger regeneration of an existing certificate (https://github.com/ansible-collections/community.crypto/pull/256). +- openssh_cert - adding ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/255). + +Bugfixes +-------- + +- openssh_cert - fixed certificate generation to restore original certificate if an error is encountered (https://github.com/ansible-collections/community.crypto/pull/255). +- openssh_keypair - fixed a bug that prevented custom file attributes being applied to public keys (https://github.com/ansible-collections/community.crypto/pull/257). + +v1.7.1 +====== + +Release Summary +--------------- + +Bugfix release. + +Bugfixes +-------- + +- openssl_pkcs12 - fix crash when loading passphrase-protected PKCS#12 files with ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/issues/247, https://github.com/ansible-collections/community.crypto/pull/248). + +v1.7.0 +====== + +Release Summary +--------------- + +Regular feature and bugfix release. + +Minor Changes +------------- + +- cryptography_openssh module utils - new module_utils for managing asymmetric keypairs and OpenSSH formatted/encoded asymmetric keypairs (https://github.com/ansible-collections/community.crypto/pull/213). +- openssh_keypair - added ``backend`` parameter for selecting between the cryptography library or the OpenSSH binary for the execution of actions performed by ``openssh_keypair`` (https://github.com/ansible-collections/community.crypto/pull/236). +- openssh_keypair - added ``passphrase`` parameter for encrypting/decrypting OpenSSH private keys (https://github.com/ansible-collections/community.crypto/pull/225). +- openssl_csr - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). +- openssl_csr_info - now returns ``public_key_type`` and ``public_key_data`` (https://github.com/ansible-collections/community.crypto/pull/233). +- openssl_csr_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/204). +- openssl_csr_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). +- openssl_pkcs12 - added option ``select_crypto_backend`` and a ``cryptography`` backend. This requires cryptography 3.0 or newer, and does not support the ``iter_size`` and ``maciter_size`` options (https://github.com/ansible-collections/community.crypto/pull/234). +- openssl_privatekey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). +- openssl_privatekey_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/205). +- openssl_privatekey_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). +- openssl_publickey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). +- x509_certificate - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). +- x509_certificate_info - now returns ``public_key_type`` and ``public_key_data`` (https://github.com/ansible-collections/community.crypto/pull/233). +- x509_certificate_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/206). +- x509_certificate_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). +- x509_crl - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). +- x509_crl_info - add ``list_revoked_certificates`` option to avoid enumerating all revoked certificates (https://github.com/ansible-collections/community.crypto/pull/232). +- x509_crl_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/203). + +Bugfixes +-------- + +- openssh_keypair - fix ``check_mode`` to populate return values for existing keypairs (https://github.com/ansible-collections/community.crypto/issues/113, https://github.com/ansible-collections/community.crypto/pull/230). +- various modules - prevent crashes when modules try to set attributes on not yet existing files in check mode. This will be fixed in ansible-core 2.12, but it is not backported to every Ansible version we support (https://github.com/ansible-collections/community.crypto/issue/242, https://github.com/ansible-collections/community.crypto/pull/243). +- x509_certificate - fix crash when ``assertonly`` provider is used and some error conditions should be reported (https://github.com/ansible-collections/community.crypto/issues/240, https://github.com/ansible-collections/community.crypto/pull/241). + +New Modules +----------- + +- openssl_publickey_info - Provide information for OpenSSL public keys + +v1.6.2 +====== + +Release Summary +--------------- + +Bugfix release. Fixes compatibility issue of ACME modules with step-ca. + +Bugfixes +-------- + +- acme_* modules - avoid crashing for ACME servers where the ``meta`` directory key is not present (https://github.com/ansible-collections/community.crypto/issues/220, https://github.com/ansible-collections/community.crypto/pull/221). + +v1.6.1 +====== + +Release Summary +--------------- + +Bugfix release. + +Bugfixes +-------- + +- acme_* modules - fix wrong usages of ``ACMEProtocolException`` (https://github.com/ansible-collections/community.crypto/pull/216, https://github.com/ansible-collections/community.crypto/pull/217). + +v1.6.0 +====== + +Release Summary +--------------- + +Fixes compatibility issues with the latest ansible-core 2.11 beta, and contains a lot of internal refactoring for the ACME modules and support for private key passphrases for them. + +Minor Changes +------------- + +- acme module_utils - the ``acme`` module_utils has been split up into several Python modules (https://github.com/ansible-collections/community.crypto/pull/184). +- acme_* modules - codebase refactor which should not be visible to end-users (https://github.com/ansible-collections/community.crypto/pull/184). +- acme_* modules - support account key passphrases for ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/issues/197, https://github.com/ansible-collections/community.crypto/pull/207). +- acme_certificate_revoke - support revoking by private keys that are passphrase protected for ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/pull/207). +- acme_challenge_cert_helper - add ``private_key_passphrase`` parameter (https://github.com/ansible-collections/community.crypto/pull/207). + +Deprecated Features +------------------- + +- acme module_utils - the ``acme`` module_utils (``ansible_collections.community.crypto.plugins.module_utils.acme``) is deprecated and will be removed in community.crypto 2.0.0. Use the new Python modules in the ``acme`` package instead (``ansible_collections.community.crypto.plugins.module_utils.acme.xxx``) (https://github.com/ansible-collections/community.crypto/pull/184). + +Bugfixes +-------- + +- action_module plugin helper - make compatible with latest changes in ansible-core 2.11.0b3 (https://github.com/ansible-collections/community.crypto/pull/202). +- openssl_privatekey_pipe - make compatible with latest changes in ansible-core 2.11.0b3 (https://github.com/ansible-collections/community.crypto/pull/202). + +v1.5.0 +====== + +Release Summary +--------------- + +Regular feature and bugfix release. Deprecates a return value. + +Minor Changes +------------- + +- acme_account_info - when ``retrieve_orders`` is not ``ignore`` and the ACME server allows to query orders, the new return value ``order_uris`` is always populated with a list of URIs (https://github.com/ansible-collections/community.crypto/pull/178). +- luks_device - allow to specify sector size for LUKS2 containers with new ``sector_size`` parameter (https://github.com/ansible-collections/community.crypto/pull/193). + +Deprecated Features +------------------- + +- acme_account_info - when ``retrieve_orders=url_list``, ``orders`` will no longer be returned in community.crypto 2.0.0. Use ``order_uris`` instead (https://github.com/ansible-collections/community.crypto/pull/178). + +Bugfixes +-------- + +- openssl_csr - no longer fails when comparing CSR without basic constraint when ``basic_constraints`` is specified (https://github.com/ansible-collections/community.crypto/issues/179, https://github.com/ansible-collections/community.crypto/pull/180). + +v1.4.0 +====== + +Release Summary +--------------- + +Release with several new features and bugfixes. + +Minor Changes +------------- + +- The ACME module_utils has been relicensed back from the Simplified BSD License (https://opensource.org/licenses/BSD-2-Clause) to the GPLv3+ (same license used by most other code in this collection). This undoes a licensing change when the original GPLv3+ licensed code was moved to module_utils in https://github.com/ansible/ansible/pull/40697 (https://github.com/ansible-collections/community.crypto/pull/165). +- The ``crypto/identify.py`` module_utils has been renamed to ``crypto/pem.py`` (https://github.com/ansible-collections/community.crypto/pull/166). +- luks_device - ``new_keyfile``, ``new_passphrase``, ``remove_keyfile`` and ``remove_passphrase`` are now idempotent (https://github.com/ansible-collections/community.crypto/issues/19, https://github.com/ansible-collections/community.crypto/pull/168). +- luks_device - allow to configure PBKDF (https://github.com/ansible-collections/community.crypto/pull/163). +- openssl_csr, openssl_csr_pipe - allow to specify CRL distribution endpoints with ``crl_distribution_points`` (https://github.com/ansible-collections/community.crypto/issues/147, https://github.com/ansible-collections/community.crypto/pull/167). +- openssl_pkcs12 - allow to specify certificate bundles in ``other_certificates`` by using new option ``other_certificates_parse_all`` (https://github.com/ansible-collections/community.crypto/issues/149, https://github.com/ansible-collections/community.crypto/pull/166). + +Bugfixes +-------- + +- acme_certificate - error when requested challenge type is not found for non-valid challenges, instead of hanging on step 2 (https://github.com/ansible-collections/community.crypto/issues/171, https://github.com/ansible-collections/community.crypto/pull/173). + +v1.3.0 +====== + +Release Summary +--------------- + +Contains new modules ``openssl_privatekey_pipe``, ``openssl_csr_pipe`` and ``x509_certificate_pipe`` which allow to create or update private keys, CSRs and X.509 certificates without having to write them to disk. + + +Minor Changes +------------- + +- openssh_cert - add module parameter ``use_agent`` to enable using signing keys stored in ssh-agent (https://github.com/ansible-collections/community.crypto/issues/116). +- openssl_csr - refactor module to allow code re-use by openssl_csr_pipe (https://github.com/ansible-collections/community.crypto/pull/123). +- openssl_privatekey - refactor module to allow code re-use by openssl_privatekey_pipe (https://github.com/ansible-collections/community.crypto/pull/119). +- openssl_privatekey - the elliptic curve ``secp192r1`` now triggers a security warning. Elliptic curves of at least 224 bits should be used for new keys; see `here <https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec.html#elliptic-curves>`_ (https://github.com/ansible-collections/community.crypto/pull/132). +- x509_certificate - for the ``selfsigned`` provider, a CSR is not required anymore. If no CSR is provided, the module behaves as if a minimal CSR which only contains the public key has been provided (https://github.com/ansible-collections/community.crypto/issues/32, https://github.com/ansible-collections/community.crypto/pull/129). +- x509_certificate - refactor module to allow code re-use by x509_certificate_pipe (https://github.com/ansible-collections/community.crypto/pull/135). + +Bugfixes +-------- + +- openssl_pkcs12 - report the correct state when ``action`` is ``parse`` (https://github.com/ansible-collections/community.crypto/issues/143). +- support code - improve handling of certificate and certificate signing request (CSR) loading with the ``cryptography`` backend when errors occur (https://github.com/ansible-collections/community.crypto/issues/138, https://github.com/ansible-collections/community.crypto/pull/139). +- x509_certificate - fix ``entrust`` provider, which was broken since community.crypto 0.1.0 due to a feature added before the collection move (https://github.com/ansible-collections/community.crypto/pull/135). + +New Modules +----------- + +- openssl_csr_pipe - Generate OpenSSL Certificate Signing Request (CSR) +- openssl_privatekey_pipe - Generate OpenSSL private keys without disk access +- x509_certificate_pipe - Generate and/or check OpenSSL certificates + +v1.2.0 +====== + +Release Summary +--------------- + +Please note that this release fixes a security issue (CVE-2020-25646). + +Minor Changes +------------- + +- acme_certificate - allow to pass CSR file as content with new option ``csr_content`` (https://github.com/ansible-collections/community.crypto/pull/115). +- x509_certificate_info - add ``fingerprints`` return value which returns certificate fingerprints (https://github.com/ansible-collections/community.crypto/pull/121). + +Security Fixes +-------------- + +- openssl_csr - the option ``privatekey_content`` was not marked as ``no_log``, resulting in it being dumped into the system log by default, and returned in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125). +- openssl_privatekey_info - the option ``content`` was not marked as ``no_log``, resulting in it being dumped into the system log by default, and returned in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125). +- openssl_publickey - the option ``privatekey_content`` was not marked as ``no_log``, resulting in it being dumped into the system log by default, and returned in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125). +- openssl_signature - the option ``privatekey_content`` was not marked as ``no_log``, resulting in it being dumped into the system log by default, and returned in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125). +- x509_certificate - the options ``privatekey_content`` and ``ownca_privatekey_content`` were not marked as ``no_log``, resulting in it being dumped into the system log by default, and returned in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125). +- x509_crl - the option ``privatekey_content`` was not marked as ``no_log``, resulting in it being dumped into the system log by default, and returned in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125). + +Bugfixes +-------- + +- openssl_pkcs12 - do not crash when reading PKCS#12 file which has no private key and/or no main certificate (https://github.com/ansible-collections/community.crypto/issues/103). + +v1.1.1 +====== + +Release Summary +--------------- + +Bugfixes for Ansible 2.10.0. + +Bugfixes +-------- + +- meta/runtime.yml - convert Ansible version numbers for old names of modules to collection version numbers (https://github.com/ansible-collections/community.crypto/pull/108). +- openssl_csr - improve handling of IDNA errors (https://github.com/ansible-collections/community.crypto/issues/105). + +v1.1.0 +====== + +Release Summary +--------------- + +Release for Ansible 2.10.0. + + +Minor Changes +------------- + +- acme_account - add ``external_account_binding`` option to allow creation of ACME accounts with External Account Binding (https://github.com/ansible-collections/community.crypto/issues/89). +- acme_certificate - allow new selector ``test_certificates: first`` for ``select_chain`` parameter (https://github.com/ansible-collections/community.crypto/pull/102). +- cryptography backends - support arbitrary dotted OIDs (https://github.com/ansible-collections/community.crypto/issues/39). +- get_certificate - add support for SNI (https://github.com/ansible-collections/community.crypto/issues/69). +- luks_device - add support for encryption options on container creation (https://github.com/ansible-collections/community.crypto/pull/97). +- openssh_cert - add support for PKCS#11 tokens (https://github.com/ansible-collections/community.crypto/pull/95). +- openssl_certificate - the PyOpenSSL backend now uses 160 bits of randomness for serial numbers, instead of a random number between 1000 and 99999. Please note that this is not a high quality random number (https://github.com/ansible-collections/community.crypto/issues/76). +- openssl_csr - add support for name constraints extension (https://github.com/ansible-collections/community.crypto/issues/46). +- openssl_csr_info - add support for name constraints extension (https://github.com/ansible-collections/community.crypto/issues/46). + +Bugfixes +-------- + +- acme_inspect - fix problem with Python 3.5 that JSON was not decoded (https://github.com/ansible-collections/community.crypto/issues/86). +- get_certificate - fix ``ca_cert`` option handling when ``proxy_host`` is used (https://github.com/ansible-collections/community.crypto/pull/84). +- openssl_*, x509_* modules - fix handling of general names which refer to IP networks and not IP addresses (https://github.com/ansible-collections/community.crypto/pull/92). + +New Modules +----------- + +- openssl_signature - Sign data with openssl +- openssl_signature_info - Verify signatures with openssl + +v1.0.0 +====== + +Release Summary +--------------- + +This is the first proper release of the ``community.crypto`` collection. This changelog contains all changes to the modules in this collection that were added after the release of Ansible 2.9.0. + + +Minor Changes +------------- + +- luks_device - accept ``passphrase``, ``new_passphrase`` and ``remove_passphrase``. +- luks_device - add ``keysize`` parameter to set key size at LUKS container creation +- luks_device - added support to use UUIDs, and labels with LUKS2 containers +- luks_device - added the ``type`` option that allows user explicit define the LUKS container format version +- openssh_keypair - instead of regenerating some broken or password protected keys, fail the module. Keys can still be regenerated by calling the module with ``force=yes``. +- openssh_keypair - the ``regenerate`` option allows to configure the module's behavior when it should or needs to regenerate private keys. +- openssl_* modules - the cryptography backend now properly supports ``dirName``, ``otherName`` and ``RID`` (Registered ID) names. +- openssl_certificate - Add option for changing which ACME directory to use with acme-tiny. Set the default ACME directory to Let's Encrypt instead of using acme-tiny's default. (acme-tiny also uses Let's Encrypt at the time being, so no action should be neccessary.) +- openssl_certificate - Change the required version of acme-tiny to >= 4.0.0 +- openssl_certificate - allow to provide content of some input files via the ``csr_content``, ``privatekey_content``, ``ownca_privatekey_content`` and ``ownca_content`` options. +- openssl_certificate - allow to return the existing/generated certificate directly as ``certificate`` by setting ``return_content`` to ``yes``. +- openssl_certificate_info - allow to provide certificate content via ``content`` option (https://github.com/ansible/ansible/issues/64776). +- openssl_csr - Add support for specifying the SAN ``otherName`` value in the OpenSSL ASN.1 UTF8 string format, ``otherName:<OID>;UTF8:string value``. +- openssl_csr - allow to provide private key content via ``private_key_content`` option. +- openssl_csr - allow to return the existing/generated CSR directly as ``csr`` by setting ``return_content`` to ``yes``. +- openssl_csr_info - allow to provide CSR content via ``content`` option. +- openssl_dhparam - allow to return the existing/generated DH params directly as ``dhparams`` by setting ``return_content`` to ``yes``. +- openssl_dhparam - now supports a ``cryptography``-based backend. Auto-detection can be overwritten with the ``select_crypto_backend`` option. +- openssl_pkcs12 - allow to return the existing/generated PKCS#12 directly as ``pkcs12`` by setting ``return_content`` to ``yes``. +- openssl_privatekey - add ``format`` and ``format_mismatch`` options. +- openssl_privatekey - allow to return the existing/generated private key directly as ``privatekey`` by setting ``return_content`` to ``yes``. +- openssl_privatekey - the ``regenerate`` option allows to configure the module's behavior when it should or needs to regenerate private keys. +- openssl_privatekey_info - allow to provide private key content via ``content`` option. +- openssl_publickey - allow to provide private key content via ``private_key_content`` option. +- openssl_publickey - allow to return the existing/generated public key directly as ``publickey`` by setting ``return_content`` to ``yes``. + +Deprecated Features +------------------- + +- openssl_csr - all values for the ``version`` option except ``1`` are deprecated. The value 1 denotes the current only standardized CSR version. + +Removed Features (previously deprecated) +---------------------------------------- + +- The ``letsencrypt`` module has been removed. Use ``acme_certificate`` instead. + +Bugfixes +-------- + +- ACME modules: fix bug in ACME v1 account update code +- ACME modules: make sure some connection errors are handled properly +- ACME modules: support Buypass' ACME v1 endpoint +- acme_certificate - fix crash when module is used with Python 2.x. +- acme_certificate - fix misbehavior when ACME v1 is used with ``modify_account`` set to ``false``. +- ecs_certificate - Always specify header ``connection: keep-alive`` for ECS API connections. +- ecs_certificate - Fix formatting of contents of ``full_chain_path``. +- get_certificate - Fix cryptography backend when pyopenssl is unavailable (https://github.com/ansible/ansible/issues/67900) +- openssh_keypair - add logic to avoid breaking password protected keys. +- openssh_keypair - fixes idempotence issue with public key (https://github.com/ansible/ansible/issues/64969). +- openssh_keypair - public key's file attributes (permissions, owner, group, etc.) are now set to the same values as the private key. +- openssl_* modules - prevent crash on fingerprint determination in FIPS mode (https://github.com/ansible/ansible/issues/67213). +- openssl_certificate - When provider is ``entrust``, use a ``connection: keep-alive`` header for ECS API connections. +- openssl_certificate - ``provider`` option was documented as required, but it was not checked whether it was provided. It is now only required when ``state`` is ``present``. +- openssl_certificate - fix ``assertonly`` provider certificate verification, causing 'private key mismatch' and 'subject mismatch' errors. +- openssl_certificate and openssl_csr - fix Ed25519 and Ed448 private key support for ``cryptography`` backend. This probably needs at least cryptography 2.8, since older versions have problems with signing certificates or CSRs with such keys. (https://github.com/ansible/ansible/issues/59039, PR https://github.com/ansible/ansible/pull/63984) +- openssl_csr - a warning is issued if an unsupported value for ``version`` is used for the ``cryptography`` backend. +- openssl_csr - the module will now enforce that ``privatekey_path`` is specified when ``state=present``. +- openssl_publickey - fix a module crash caused when pyOpenSSL is not installed (https://github.com/ansible/ansible/issues/67035). + +New Modules +----------- + +- ecs_domain - Request validation of a domain with the Entrust Certificate Services (ECS) API +- x509_crl - Generate Certificate Revocation Lists (CRLs) +- x509_crl_info - Retrieve information on Certificate Revocation Lists (CRLs) diff --git a/ansible_collections/community/crypto/CHANGELOG.rst.license b/ansible_collections/community/crypto/CHANGELOG.rst.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/CHANGELOG.rst.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/COPYING b/ansible_collections/community/crypto/COPYING new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/ansible_collections/community/crypto/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/ansible_collections/community/crypto/FILES.json b/ansible_collections/community/crypto/FILES.json new file mode 100644 index 000000000..1b50debec --- /dev/null +++ b/ansible_collections/community/crypto/FILES.json @@ -0,0 +1,5122 @@ +{ + "files": [ + { + "name": ".", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".azure-pipelines", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".azure-pipelines/scripts", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/aggregate-coverage.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e468f84219edab8cf96c5f785b97533c954ae441eb8a3ecdc78465f0610fe85c", + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/combine-coverage.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "49d3567c21d253290b5c075e250b1460ea46c3f33b7d25a8994447824aa19279", + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/process-results.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9431997ce33a94d4c5d3b13be6958762087f7fc18785b92a7977f6f55d5b99d5", + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/publish-codecov.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "31c38e5c2f6021b3762c52c5c55f9fb565586b69e998b36519d3072608564546", + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/report-coverage.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bc7a84929a59e74c09c3e59e0fabbb6888538b4dc6a4dd6ec7ef554a64cc10b6", + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/run-tests.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "237b5604b5f4581824888ef0c2c58594248fa05c46e59b502b525d80c90ffd92", + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/time-command.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "486dd0a00417773b1a8901b4d411cb82f7e3ffea636ed69163743c4b43bf683a", + "format": 1 + }, + { + "name": ".azure-pipelines/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".azure-pipelines/templates/coverage.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "64e463d7bc905405658478276ec947084df99b36512a6429cd991d3a8fddf7b2", + "format": 1 + }, + { + "name": ".azure-pipelines/templates/matrix.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b082277884eb4e4392647491e6a390fdce58e66f036f00b104cfb35e6d210bc", + "format": 1 + }, + { + "name": ".azure-pipelines/templates/test.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "21b5fa07d5f0bca65f95e337ce815fd62ffd941e9846a5322fa606eab76ecec5", + "format": 1 + }, + { + "name": ".azure-pipelines/README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1e68364750709fae75220652b5837198a1deff224fa37d4147eec37a7bcddd70", + "format": 1 + }, + { + "name": ".azure-pipelines/azure-pipelines.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "691ac4e41aaa53e9b556a626fac186a38637ccb41093d0d15023fa80b550fc85", + "format": 1 + }, + { + "name": ".github", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github/workflows", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github/workflows/ansible-test.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7b70d3a4a411debb31f68a1d6d2e22d94f871e0c48f356728aa679b003c5b04e", + "format": 1 + }, + { + "name": ".github/workflows/docs-pr.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "abea9c2ddb6796144762046544efd93c3aaca925e2bc38d2f95f1849cfc0622a", + "format": 1 + }, + { + "name": ".github/workflows/docs-push.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "48c2da777eadd9ff8cbe9ff4c4bb147fc95d9f3f4e46a7a7b2bfa4c706ee350e", + "format": 1 + }, + { + "name": ".github/workflows/ee.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "29b85993d26447dc9aba53b7a8cfbc997125595de343fe943e7a701ecd3b928a", + "format": 1 + }, + { + "name": ".github/workflows/reuse.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7642b7c0999df1190506e7d73be3ea67f5e7f9bb78e809951a96b2f5418a8798", + "format": 1 + }, + { + "name": ".github/dependabot.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f314f2fce46d346ef9559fdeae3a55ce6d799bab45a255092446c31e17e6874d", + "format": 1 + }, + { + "name": ".github/patchback.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6a23e48e2562604318540e6ddcac75213ad2c367258d76fc75914e9b939d380e", + "format": 1 + }, + { + "name": ".reuse", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".reuse/dep5", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9d8cb20d72d7e81aaf2e7f0ddce7eacdb3d47a890541717d7fae08a6cab7ebed", + "format": 1 + }, + { + "name": "LICENSES", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "LICENSES/GPL-3.0-or-later.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986", + "format": 1 + }, + { + "name": "LICENSES/Apache-2.0.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "aac73b3148f6d1d7111dbca32099f68d26c644c6813ae1e4f05f6579aa2663fe", + "format": 1 + }, + { + "name": "LICENSES/BSD-2-Clause.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f11e51ed1eec39ad21d458ba44d805807a301c17ee9fe39538ccc9e2b280936c", + "format": 1 + }, + { + "name": "LICENSES/BSD-3-Clause.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "602c4c7482de6479dd2e9793cda275e5e63d773dacd1eca689232ab7008fb4fb", + "format": 1 + }, + { + "name": "LICENSES/PSF-2.0.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "83b042fc7d6aca0f10d68e45efa56b9bc0a1496608e7e7728fe09d1a534a054a", + "format": 1 + }, + { + "name": "changelogs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "changelogs/fragments", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "changelogs/fragments/.keep", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "changelogs/changelog.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "75e5ff73018cb4f36565a129cffe94719e80e7a7c0704195416962eac2d6888e", + "format": 1 + }, + { + "name": "changelogs/changelog.yaml.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "changelogs/config.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "35eb45f26e27a9f2830eb5cd398b976e603c12a7e302e5748b2860782f8ebe76", + "format": 1 + }, + { + "name": "docs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "docs/docsite", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "docs/docsite/rst", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "docs/docsite/rst/guide_ownca.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2da13087991e7e9c56edbc1f55a974953d40c85516135a02ec39df147d82ab4b", + "format": 1 + }, + { + "name": "docs/docsite/rst/guide_selfsigned.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ba38bd4f0c1e7e96d051be283484f97b5a317f437720f178c13990798ce5510a", + "format": 1 + }, + { + "name": "docs/docsite/extra-docs.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "589a429a3861429cbf4a67ac4903e476889a2780530b0ab167b34344541a5c7d", + "format": 1 + }, + { + "name": "docs/docsite/links.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "77bbf987fc026d97f0b6ab10ba4f10efc26aa65648a2d3ebce2b16a8f01178d4", + "format": 1 + }, + { + "name": "meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "meta/ee-bindep.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2bb12adf92d90ab1d0669a80f1cf07a4ad245d12d2263e9470fd8e49c35a314a", + "format": 1 + }, + { + "name": "meta/ee-requirements.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "54ef2a116b8d0168787fb2e59fc6c1cc42097409d2482334e5f8ca4313854de3", + "format": 1 + }, + { + "name": "meta/execution-environment.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f42b4946cc8e628812ba87c2d36dbd5ed7af316d5c68bba8fd7d72808254f967", + "format": 1 + }, + { + "name": "meta/runtime.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "78f0817cd6a1f61ef403cff6f944799c473b2fe01fc618f8e4b94a996af77d75", + "format": 1 + }, + { + "name": "plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/action", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/action/openssl_privatekey_pipe.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b5fa69f89faba559bb4530aa3b4ae1e0ac8faa7b0cf242a3b155ca8fce52aeff", + "format": 1 + }, + { + "name": "plugins/doc_fragments", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/doc_fragments/acme.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "db00eecb26e0877afdaa69303adeee2f8b36ba7566c5729e0bee15458c100d55", + "format": 1 + }, + { + "name": "plugins/doc_fragments/attributes.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "95d32c9378cf134c477f585f5f788fe72c07f01503c88f8c521428483f47e3a8", + "format": 1 + }, + { + "name": "plugins/doc_fragments/ecs_credential.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "938283f54398840db2726b73b6c8e14da81d45c076070a73238707c42dcd537d", + "format": 1 + }, + { + "name": "plugins/doc_fragments/module_certificate.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8dfa16820dd58a10c3480ff33bb2a80d59d49d7ff69e8bb87f0d489690a5bb05", + "format": 1 + }, + { + "name": "plugins/doc_fragments/module_csr.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "088ff6848934ec374afd0947e41e1ba180f1e22c306736550f8abb7348337b19", + "format": 1 + }, + { + "name": "plugins/doc_fragments/module_privatekey.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0228ad1f1652630271882932537c14c833eca5303f77cfd31024e6026f92b6c0", + "format": 1 + }, + { + "name": "plugins/doc_fragments/module_privatekey_convert.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "29872291df81d1b07f8bd474255cec6af84069dd9ba958205630e9b5e0ee6d71", + "format": 1 + }, + { + "name": "plugins/doc_fragments/name_encoding.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "51f5cfce1cce8d055ae05979965dcfa4384056ad01862a1d3a24b71c0841e90b", + "format": 1 + }, + { + "name": "plugins/filter", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/filter/openssl_csr_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1dd1bf8947cf3de522d52922c863814d88f4f4ffd16e63ad498bc188047180ea", + "format": 1 + }, + { + "name": "plugins/filter/openssl_privatekey_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9b3b7098951f0349af2c48c1ab4dd3169949110dce189fa9a12397a66fcca914", + "format": 1 + }, + { + "name": "plugins/filter/openssl_publickey_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0e5a2bf5b101d83b5e6cce4b945c9cb3aec35a0a0869770de8c329a29575d733", + "format": 1 + }, + { + "name": "plugins/filter/split_pem.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8de65adc15ce5f8a36cd99a3b0990889805cda17150742a704ba13ed799db678", + "format": 1 + }, + { + "name": "plugins/filter/x509_certificate_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "61cd93f0f99d5f9d8e777ceab8388ef9b5de24cdd3e30bd19b9774ce8ab7ce34", + "format": 1 + }, + { + "name": "plugins/filter/x509_crl_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "22d3c47a885a4d56993ed29f5b8df9fd861c61290a9b888d807c4f4bbeec4a40", + "format": 1 + }, + { + "name": "plugins/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/acme", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/acme/account.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d4b0ab4bb75e8d8b36f1dbe67b3c8a19fb40741fe332d11ccef3ae0d321390fd", + "format": 1 + }, + { + "name": "plugins/module_utils/acme/acme.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "06671258b9f97cc7666e6598baf77d30bfc84fcc911594debae90d9322a1ef94", + "format": 1 + }, + { + "name": "plugins/module_utils/acme/backend_cryptography.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd7551ac178d54d692a0028da9e1bf7075834f13c42230ff12b1253239c5590", + "format": 1 + }, + { + "name": "plugins/module_utils/acme/backend_openssl_cli.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5e5e5127e8eb261168dfe60dcd1e1fd63ad1196ca5947a1845c3f7481dcaf431", + "format": 1 + }, + { + "name": "plugins/module_utils/acme/backends.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cf618865eb9f569e5d775d0539463ce6f7abd3cd593f4528a83f3f1bfb2885e5", + "format": 1 + }, + { + "name": "plugins/module_utils/acme/certificates.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "18d85523fd18a82c47a4a918992e5c80adcd7c50ff07e1c592087821a1f5b672", + "format": 1 + }, + { + "name": "plugins/module_utils/acme/challenges.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f425838b07c8ec95b9ea9afe07e58f724c7ef5cb1a876e1dcfa9e4997d7f31ab", + "format": 1 + }, + { + "name": "plugins/module_utils/acme/errors.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "21be07a9794843ca25183a8827414b50f9775f7808eeff24bb5b35686f12f180", + "format": 1 + }, + { + "name": "plugins/module_utils/acme/io.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d8fb81ac7e39595b46f23b60826c9ce53dca5b2eeb86d7268e39655b3de40988", + "format": 1 + }, + { + "name": "plugins/module_utils/acme/orders.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bc139936871d65b9f4c0b0e5b772114eac5103bc291c4db907b650d0da63ea0d", + "format": 1 + }, + { + "name": "plugins/module_utils/acme/utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e4856fa1ff751214f8fabdb30c4813191e891b18c778439913fef72e5c421267", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/certificate.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "707b1e0abbd35ffb76d00744ee35b4f5521b7184de04d587f34cc256aa1a7728", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/certificate_acme.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d72d7415d2c04b2626146f3387769be3ae652370351872ec16058c86d8b083c9", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/certificate_entrust.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0046918bf9e870753fb5a24815ada1dcc42077f2ba62e950942f26c36a298b62", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/certificate_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "20a96ca9b95f652564f49dce0e477a314da3f6cc1e6f51e3ae75992e6f54da5b", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/certificate_ownca.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8491949f4ed06374834f8ba4cd67edac9bbbeae47247b0e978c92220aaf3cb50", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/certificate_selfsigned.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c535db42e5267798cb1d12c1329ed3e526ede4afae8dcef10d619e779535ecf6", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/common.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7624320b1ddd2c5661df4ace026ed0b29282125755951e13356dab6f8efba374", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/crl_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0cc4d1ece37dee2606d3d099f7d895d14e1bb4f5fe292d0e5657021041cef41b", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/csr.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b3a7c30f3f7aaedb29f3e493fc6020c135cb0827c8e7bd53c49c68b3e97277da", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/csr_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a3004b61c400e52335d4360d7b38397795123b0d8ae038904ecb9b0667bf6b71", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/privatekey.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "40ee4181c816995341ea4bd07d3de0d4c19b3d2076f7d6903fa15fc803543aff", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/privatekey_convert.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5f4d6daeed1159cbd3d3b6f3d51094b3b559cf22726364bcb392627bb90f7e1f", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/privatekey_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7bc30ee7a4aa3bc0a8b52f38b4167382adeacba91c0bb68448cf7428f05d5225", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/module_backends/publickey_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8f8869a75f9592f7838c4b1ed1d02f1722bd832695abc6d897a69cd28fffcee2", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/_asn1.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f3b9e7e9f1a774aee78981875144b9c8bfda7372b17c86278820da6e677ffbd8", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/_obj2txt.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2af793c93405ec6206abea5b1858456f4c51dd6c997241782a94d084826aa69f", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/_objects.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a55aa7f55f491592adf8f2be833555c5b27d9533b472002dc0aa2b7f1055bece", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/_objects_data.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e785c4858510c1be454ff5f4981ab89712419ab3681eceb64df7038371ac75be", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/basic.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "97c109dcea2285d58eed0f284ccf4bb8ca5b15e2941c8f203d41dec78ef86175", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/cryptography_crl.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "070fbd85edef9007d52574252b31f189dabc6256b828251075d506e6f1186082", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/cryptography_support.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "677a5f62a270179a8adab19ba2c91b894c47d90aba0a93312a13713e9f82fe1c", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/math.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6d885ed9fd96054252e527d806846b42ebbb81fa0dbbd024eced265f02e5adb6", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/openssh.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "baf54f73dc367af5db2d954fdd803ff6a7766b70d77eee0251de6ab7ba12a4c6", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/pem.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9ea343de8d832afccb480a30b64514d152cbf0c5bd435bc2b56ac88e8f35a2b1", + "format": 1 + }, + { + "name": "plugins/module_utils/crypto/support.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "af8b7e736a0383370681a6a9017a9e9f3c9bbcf324c3b6c2bc99e5031bef3183", + "format": 1 + }, + { + "name": "plugins/module_utils/ecs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/ecs/api.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "116b6a16f3d5afb5359d916e3e1be3d65d6124d912f37bb5c5e81bf47b1b9053", + "format": 1 + }, + { + "name": "plugins/module_utils/openssh", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/openssh/backends", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/openssh/backends/common.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ae444b21b047d77b8638a42041b70d6053b9950161d0fca7eceacff4e16e8130", + "format": 1 + }, + { + "name": "plugins/module_utils/openssh/backends/keypair_backend.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bf2830ac27e85ee6b0955a9361aa4f344576a809ef6a36f90b6a30f5736a5c8c", + "format": 1 + }, + { + "name": "plugins/module_utils/openssh/certificate.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3edc83078c5dee008ccb961123d202b4ea2340772f28137dcef60e945604464c", + "format": 1 + }, + { + "name": "plugins/module_utils/openssh/cryptography.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "879ad2804fda57797ecbb83c72ad7ab8fe0b0cb90fe79c3de88c61a710b0c2c4", + "format": 1 + }, + { + "name": "plugins/module_utils/openssh/utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d35c068a7836ea564e23d20d650f4ad4dad1e5017418877ff5a08844152c1477", + "format": 1 + }, + { + "name": "plugins/module_utils/_version.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "44a3a049aaa06add6402c1091c1f0c57664b3c1a928ec3b0651fe0b930c71e4b", + "format": 1 + }, + { + "name": "plugins/module_utils/io.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "590b133f8301451cc145ae6b58c01870291f7993b2ebc40599813718f5e95d96", + "format": 1 + }, + { + "name": "plugins/module_utils/version.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a633151e8a76c7f36703fd1c20f65c9b2cb0128fa3aa6c804ef70fe1fab56fb2", + "format": 1 + }, + { + "name": "plugins/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/modules/acme_account.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2be0d33350136b19a27ddfef23035739747b1427d55cdc6f11c3e94ba49b9535", + "format": 1 + }, + { + "name": "plugins/modules/acme_account_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "246523550f4005e2c89a5fcc192afa2b2531f9cf028ab9ac82733c66dc47bbe0", + "format": 1 + }, + { + "name": "plugins/modules/acme_certificate.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b306d1dd602e77d49134a731ba03d0b1ec298255e3937cdbf53c86e4e94c7d3d", + "format": 1 + }, + { + "name": "plugins/modules/acme_certificate_revoke.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4d18dcaa1b2841a58f4a1e8495394d07deb29f2343695382d81fd406af9748b5", + "format": 1 + }, + { + "name": "plugins/modules/acme_challenge_cert_helper.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0526e703b1544c6c9aa1bd8a5b35b7e5476134abc64ffa74635ef6b865abf303", + "format": 1 + }, + { + "name": "plugins/modules/acme_inspect.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "46a7b696d23763c8d5337a2c2278453bf32685281c682fbeef6fb5fa38779e90", + "format": 1 + }, + { + "name": "plugins/modules/certificate_complete_chain.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c15e21508e5c6bc6bd8e9c8849a80e202cfb82efc8ff32216ccbebc68ec327f3", + "format": 1 + }, + { + "name": "plugins/modules/crypto_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "733397087187d0f1247e076092f3b049501bf9c57a1d0a3381ab19911d93d3a2", + "format": 1 + }, + { + "name": "plugins/modules/ecs_certificate.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "691ac56b47be423a44b0443f85024294aeb2ebe9bf5001539429560005efea50", + "format": 1 + }, + { + "name": "plugins/modules/ecs_domain.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "296ec6b955ab523fe48f4a48501344b1eb3dd7446321fa12b28d08001cbf4956", + "format": 1 + }, + { + "name": "plugins/modules/get_certificate.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "07a3bbe0329e3f5ac80fda788852813d3c384c98b47cbb85ef93cec27564dbe0", + "format": 1 + }, + { + "name": "plugins/modules/luks_device.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "040c47f4ca02ecbb06e165ca193ee7193ea9d3f406f23b64112bd50d5abe3905", + "format": 1 + }, + { + "name": "plugins/modules/openssh_cert.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ebe08a5b115f9ce7a935d9858977174d959875ec6cc3a01ca35d5689ae7f9c46", + "format": 1 + }, + { + "name": "plugins/modules/openssh_keypair.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0ff1f3cadd6721dfbd049f2e4ba82fba39cefcd647f9cec7d14919562dcb6485", + "format": 1 + }, + { + "name": "plugins/modules/openssl_csr.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0f9602fb35216d8cbd881f6f8081cf8a865bf1bdeec0c88099f950cbba332314", + "format": 1 + }, + { + "name": "plugins/modules/openssl_csr_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ab9a98810711a5d775bd97ef3afd943820fa8ca0c09c2fd8e4c9d9686891122d", + "format": 1 + }, + { + "name": "plugins/modules/openssl_csr_pipe.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ed304374255a30099db7d7920027ba5fb3ac47fb2358ec0ec729fc7ed36d7638", + "format": 1 + }, + { + "name": "plugins/modules/openssl_dhparam.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2c9a0b0dbce23f05873ecb65e82d57208fe91480200ba19cdfc4ad67344df15b", + "format": 1 + }, + { + "name": "plugins/modules/openssl_pkcs12.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fa4234411070092e3d6244edceb1406d8877a138c4e7e1480541625f107b9123", + "format": 1 + }, + { + "name": "plugins/modules/openssl_privatekey.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a3e09f1db6db28aefd0dbf436af986adb3c2c7eb71157ed9012b06fcda659e2e", + "format": 1 + }, + { + "name": "plugins/modules/openssl_privatekey_convert.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c772ee82bbce0a2c4dfc0dfa4ed72ff99763da84ed6391f5ce2abe6a3af63565", + "format": 1 + }, + { + "name": "plugins/modules/openssl_privatekey_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0fe21ba6d3e5966bc4335058549bb67632222c9989b1355808404b38e27bc8e4", + "format": 1 + }, + { + "name": "plugins/modules/openssl_privatekey_pipe.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dd514e1366f8cf11702a1124c683575c43a54626542069a8a23e8d004da763b4", + "format": 1 + }, + { + "name": "plugins/modules/openssl_publickey.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d656ff71493152010c203a46b0f36c6fca9312e2c3b412ea3b9ec39d4fb95a90", + "format": 1 + }, + { + "name": "plugins/modules/openssl_publickey_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e999f1e2cbb54f099c6733d66f74bbc405731b5bc6dd311be13a53c3cc7f3aeb", + "format": 1 + }, + { + "name": "plugins/modules/openssl_signature.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "30f457dcd3be6efbf405d842f244d0627397f71001106b48812d4c6bf0b75d7a", + "format": 1 + }, + { + "name": "plugins/modules/openssl_signature_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bd092d8e5bebe70c888eee8091ed83d2821debffcf69452d20d4c37d261f3742", + "format": 1 + }, + { + "name": "plugins/modules/x509_certificate.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9201740cdaa97912c7a488b93a8bf25b65a355c0275b0d41a55e99e6208b15b4", + "format": 1 + }, + { + "name": "plugins/modules/x509_certificate_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "32efd98d7efbac173a75af7f8bf2b3cf0ab65db22dc1f409e29d969e20429174", + "format": 1 + }, + { + "name": "plugins/modules/x509_certificate_pipe.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c022742c244d4cf25451b6021ae656ed76300d815a160d3148bf644f7c8a7e03", + "format": 1 + }, + { + "name": "plugins/modules/x509_crl.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0818a26da959f7d29da9334e7d0a413b5c10a26f5468889e7c28b85dabe11847", + "format": 1 + }, + { + "name": "plugins/modules/x509_crl_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "35b6cf60c8e00ab3e5fc8e49f9c060729d340664610237c0d1ede057a7adf722", + "format": 1 + }, + { + "name": "plugins/plugin_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/plugin_utils/action_module.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "99760f08be1eb62ebe1472d94e6eda888437ab3715f6ccf21d7fa796be7d643b", + "format": 1 + }, + { + "name": "plugins/plugin_utils/filter_module.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1940aa145260be843cf4e7f8a6ea93a33a1e131c1e9f37ac215d2bbcd498b8a1", + "format": 1 + }, + { + "name": "tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/crypto_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/crypto_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/crypto_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "302fc964fa45633054548c3663ffb834dc2f900a849f4a47d695124b8d2d9fd2", + "format": 1 + }, + { + "name": "tests/ee/roles/luks_device", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/luks_device/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/luks_device/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "51dba59dd1e9c505cb98a0b633ca52b6be77aaa560afe42b36d4fcadf8eb4608", + "format": 1 + }, + { + "name": "tests/ee/roles/openssh_keypair", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/openssh_keypair/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/openssh_keypair/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8df621fa40c4e47eff654b608a91e395a7c1d883276f232b9da5f8f70e447e50", + "format": 1 + }, + { + "name": "tests/ee/roles/openssl_pkcs12", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/openssl_pkcs12/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/openssl_pkcs12/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2edf83ead1f5d5f2b5ed8c7d9ba6fc2e0c109bdeddd177d30fe3626f4e1e4748", + "format": 1 + }, + { + "name": "tests/ee/roles/openssl_privatekey", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/openssl_privatekey/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/openssl_privatekey/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dd1c4bedc22280738ddc99ab5c668e77e5b1c23c47dbb3dea3067201d70a7f79", + "format": 1 + }, + { + "name": "tests/ee/roles/smoke", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/smoke/library", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/smoke/library/smoke_ipaddress.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dc144a99fd521db2f52980f205e08aad40ee85918c646f652b4d098451257d21", + "format": 1 + }, + { + "name": "tests/ee/roles/smoke/library/smoke_pyyaml.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3749bc77b7a99f2a89bf01a314156745696fa73e44cd6da29e92578977bd561b", + "format": 1 + }, + { + "name": "tests/ee/roles/smoke/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/smoke/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7b81c080581e761a670b3aa165cf0cde614eaba2c3b334d5e3306d1fcd17ae08", + "format": 1 + }, + { + "name": "tests/ee/roles/x509_certificate", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/x509_certificate/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/x509_certificate/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a4397ac119c91dd9f19229fccee6ce2df7d17d72b2cc1cbac33fa7615688ed89", + "format": 1 + }, + { + "name": "tests/ee/all.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8f5ef633045440c2562897e9fa77b60c2f4fa6101abc8e4f9ce8220f22915837", + "format": 1 + }, + { + "name": "tests/integration", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6f5821968427f3784e3f2ed2bf186dd0b0c198dabb3129c58aa1a5b5d487065d", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fbd6f6ee712ffc3d20696bc1a2650aecd2b546d7e28b22011e26d592d9ebeab8", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c3f80b8fa9ebbd81fa9674493ba9bc538f90f23d0e44a0262e772804714e29ba", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account/tests/validate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4d4d12b41a54454c998017176601999ad43737852f73135c7a1af8054b28d82a", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1846e9b081766a9ad0f4c13f42d014e82cd235f0cf8b1fd8d7eb41287c6a165f", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6f5821968427f3784e3f2ed2bf186dd0b0c198dabb3129c58aa1a5b5d487065d", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account_info/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4e9a444060e1c0a457b7dbd187cce87ee998d7b1fc6601ed8450a636b6854edc", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c3f80b8fa9ebbd81fa9674493ba9bc538f90f23d0e44a0262e772804714e29ba", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account_info/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account_info/tests/validate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "647c4e8bdb16f083114708d94a94759280327e22bad76bab14ef14169dc1da5d", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_account_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1846e9b081766a9ad0f4c13f42d014e82cd235f0cf8b1fd8d7eb41287c6a165f", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "933a27a2f645a3ade66cf95034893a8ec058cc68d9c971619937b84923e82c8c", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate/tasks/obtain-cert.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4ad45f93e26e6683cee0b57a66ddabe0147dd5d3af96fadc2209ab4d501b407a", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "41f77a7dae471c0af3d4dad849d37ed6a3ca0fd0add3723c574399cc1e9396d4", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "920be075c24392b99bc24f77f2b6dba9a0d51644e8062b0a27cdfd8fc83f05dd", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate/tests/validate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1fcc6947f28ff0b823bef72f01b2f2c8bca32ce10476589befaed6b9fe64056c", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1846e9b081766a9ad0f4c13f42d014e82cd235f0cf8b1fd8d7eb41287c6a165f", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate_revoke", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate_revoke/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate_revoke/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6f5821968427f3784e3f2ed2bf186dd0b0c198dabb3129c58aa1a5b5d487065d", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate_revoke/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate_revoke/tasks/obtain-cert.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4ad45f93e26e6683cee0b57a66ddabe0147dd5d3af96fadc2209ab4d501b407a", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate_revoke/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2b684372df6e3c773e256a0bd6581a6f24ff415c080ccef5e5b6116231e5b874", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate_revoke/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c3f80b8fa9ebbd81fa9674493ba9bc538f90f23d0e44a0262e772804714e29ba", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate_revoke/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate_revoke/tests/validate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5c15122ec5888f6a3978e37b08cc3f355b7be1aa65800369547f928a71a59244", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_certificate_revoke/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1846e9b081766a9ad0f4c13f42d014e82cd235f0cf8b1fd8d7eb41287c6a165f", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_challenge_cert_helper", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_challenge_cert_helper/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_challenge_cert_helper/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6f5821968427f3784e3f2ed2bf186dd0b0c198dabb3129c58aa1a5b5d487065d", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_challenge_cert_helper/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_challenge_cert_helper/tasks/obtain-cert.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4ad45f93e26e6683cee0b57a66ddabe0147dd5d3af96fadc2209ab4d501b407a", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_challenge_cert_helper/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "544305bba02e4b01eb3952a852af5b775e733ab585b299f0dcf942d5bb9d8891", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_challenge_cert_helper/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1846e9b081766a9ad0f4c13f42d014e82cd235f0cf8b1fd8d7eb41287c6a165f", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_inspect", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_inspect/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_inspect/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7786e51a30b0d7c8a927db7338c4d3471199f385bb2a4d480019f2e3d6eb6ff7", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_inspect/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_inspect/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d8366b412ff8b4002d339b97fb4b7b7ee9475120e0e6890a464c3701aeef45ac", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_inspect/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c3f80b8fa9ebbd81fa9674493ba9bc538f90f23d0e44a0262e772804714e29ba", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_inspect/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/acme_inspect/tests/validate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "84264754f5927eb185ed69c2f3a2f9cc67e70d611d105cde20e05d8dc9e14eb5", + "format": 1 + }, + { + "name": "tests/integration/targets/acme_inspect/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1846e9b081766a9ad0f4c13f42d014e82cd235f0cf8b1fd8d7eb41287c6a165f", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/roots", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/roots/COMODO_Certification_Authority.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e273097c7c57cb7cbb908057991ae1774d4e1e8c6a062fb6be9e6645b32fb431", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/roots/COMODO_ECC_Certification_Authority.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d69f7b57250536f57ffba92cffe82a8bbcb16e03a9a2607ec967f362ce83f9ce", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/roots/COMODO_RSA_Certification_Authority.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "24b0d4292dacb02efc38542838e378bc35f040dcd21bebfddbc82dc7feb2876d", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/roots/DST_Root_CA_X3.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "139a5e4a4e0fa505378c72c5f700934ce8333f4e6b1b508886c4b0eb14f4be99", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/roots/ISRG_Root_X1.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "22b557a27055b33606b6559f37703928d3e4ad79f110b407d04986e1843543d1", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/cert1-chain.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "077752b681576a96a949c94c83cb53c3ad5e6045f30ca7a9228ddba13e080ac5", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/cert1-fullchain.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e8e80e82ce39faeb2961f050f57efd13b648b3269f235592f2f8bcfecb576e69", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/cert1-root.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d69f7b57250536f57ffba92cffe82a8bbcb16e03a9a2607ec967f362ce83f9ce", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/cert1.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "97546a3c7cd92e96e6bd4e1f282102b0c3ae99cc75330f78d4c04633c080d74c", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/cert2-altchain.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e231300b2b023d34f4972a5b9bba2c189a91cbfc7f80ba8629d2918d77ef1480", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/cert2-altroot.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "22b557a27055b33606b6559f37703928d3e4ad79f110b407d04986e1843543d1", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/cert2-chain.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e446c5e9dbef9d09ac9f7027c034602492437a05ff6c40011d7235fca639c79a", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/cert2-fullchain.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b89e9ea8612096f642b644fddbefecc54830bbc1a321facfee7a2b888873a435", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/cert2-root.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "139a5e4a4e0fa505378c72c5f700934ce8333f4e6b1b508886c4b0eb14f4be99", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/cert2.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e356f2747f331e35160a278a2d90e619e095cb6beba645692dfd6ed75640d426", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/files/roots.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c0c90383dfe56da48ec157bd7e1b026f855a199e22fa090beadc77c931d456f2", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2d24f42729d7a600072fbc925970d9ea30708df6a973cb10b1247ce2435af852", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/tasks/create-single-certificate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2a97bc5ad0b3883a123e0b435825512a960b662d15908eb38bea79014440c680", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/tasks/create.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1ef68f0eef1b3c1c2bf6cc9414c8c93e31a5240973c91e49a85b454bf70bb38e", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/tasks/created.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8c237ec273b5edfa2e8341d828a03307028afd00894dd6bf08c80dddba1eea71", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/tasks/existing.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d07a3cbad3e5b5cfe339723d3d3437d3f1e81ac04e83f2d9909d0f85aff163de", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f06fef18e53dc5b9adde4f47dab1d164886de7874fd98ed2640253c49ad4d7c9", + "format": 1 + }, + { + "name": "tests/integration/targets/certificate_complete_chain/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9ac45ba2c69c8b401fcc628d5fcf85c39539bfa659c78c386e8876e681352eb6", + "format": 1 + }, + { + "name": "tests/integration/targets/crypto_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/crypto_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/crypto_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "637963c1711de45e717e58274c955ac5015b9127222e264f02f9008443e5db82", + "format": 1 + }, + { + "name": "tests/integration/targets/crypto_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/crypto_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "02fe545a446843b0e0091a45ed58e19b45c74ae0a530a9586a17e78c22fda8b3", + "format": 1 + }, + { + "name": "tests/integration/targets/crypto_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e8e108034382b2746739926810ffc1a9ff257d85e73e95ddaedfa1a6cd221179", + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_certificate", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_certificate/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_certificate/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "277e6949241d3d2080f02d7796025eac4f32db7b309fc9e0e485ac2b36b3ca00", + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_certificate/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_certificate/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e59ccf1b107774a3da158a6103de9695312dcf81ac29c86c69eb2f2841358791", + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_certificate/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_certificate/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3f8be447b8d1be7b60b8015b7a638316bd27118bc855c2ffe6873a35e73717b", + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_certificate/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_certificate/vars/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "60b1aaf9b53cf7eca15b7944ad29d3e8191608f9d08437470ebd34a019914d1b", + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_certificate/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9c006980ff06117f9172c933b5f21f3377e4d790cde5f2f21c9728b2eb5534f5", + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_domain", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_domain/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_domain/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "287d789bb91a040dfbaff2fab347458241380ee22b16311fab1b50cd7c6f52d1", + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_domain/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_domain/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "71992e570b4236969b2e2ede235f21cafda10f6784e545e7243cd10957f4c855", + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_domain/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_domain/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "86027f5610466a9a1fd4c8f23edf965a71b67a99458410a9640465c6e35d6036", + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_domain/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_domain/vars/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ed047836ec694bd5c8352395d574054934d9a491dd3919f0e7920eef3e915372", + "format": 1 + }, + { + "name": "tests/integration/targets/ecs_domain/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9c006980ff06117f9172c933b5f21f3377e4d790cde5f2f21c9728b2eb5534f5", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_csr_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_csr_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_csr_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e550791b9b9cf709c4dbec99b2fd8603263883e7eab8c8d99805c2aa4bcaa8a2", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_csr_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_csr_info/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a4b8bd6646d34cd56c61df0698ae0c3f88a500fbcae5d72fc5e93919c7c2697d", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_csr_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "300bc12646bf6c01fa337a744746045475705610d43ce30d5931eb9b5212dda1", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_csr_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_privatekey_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_privatekey_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_privatekey_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e550791b9b9cf709c4dbec99b2fd8603263883e7eab8c8d99805c2aa4bcaa8a2", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_privatekey_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_privatekey_info/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b9b632af1e7a038d0aa64e8a8e8381c3e65422059e314b8aa0386d9c950af05f", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_privatekey_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "339a5e5203946b4252322d1f731218c0613a25e06c4625b10a52e6d9b67e8a6a", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_privatekey_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_publickey_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_publickey_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_publickey_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e550791b9b9cf709c4dbec99b2fd8603263883e7eab8c8d99805c2aa4bcaa8a2", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_publickey_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_publickey_info/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2dd64bc04c30838b9df654caf0f4070b934116db8d63eadd8ae0d4bf0af7fa7d", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_publickey_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "adeca49b622606c3df29169e3ac7cfad90ca2415af0bf30ffe50f0e648db6b3b", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_openssl_publickey_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_split_pem", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_split_pem/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_split_pem/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f45ffd9bb071fe1b3e509473dcdd794b1cb5e5a523f3a7ed28d12dc215904767", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_split_pem/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9ac45ba2c69c8b401fcc628d5fcf85c39539bfa659c78c386e8876e681352eb6", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_certificate_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_certificate_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_certificate_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e550791b9b9cf709c4dbec99b2fd8603263883e7eab8c8d99805c2aa4bcaa8a2", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_certificate_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_certificate_info/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7d50420763aa1a10ccf2b26beecc859ed66a0624b6f4b1c54b8ebfe02eed672e", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_certificate_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a1f918e3868523e5ab27d1bc8b45e8b99e6c52310959a374a04b540b730dace1", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_certificate_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b47dacfa970ba5165ce02fbdb746b6fa9ff91c2591e094022c37deaeaaace5dd", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_crl_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_crl_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_crl_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_crl_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_crl_info/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6b69852ac0705c9b5f3a8d0f16b9207c3464c2788cbde525def0af5682e2fb70", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_crl_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "45eb2537b0351e41f2ef3a0bc6e1c293748c6cb2ee6ed08078c45222ee4af6fe", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_x509_crl_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/get_certificate", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/get_certificate/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/get_certificate/files/process_certs.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c89f526dac4b6508f52bb4dff634b9d0f96ba4c2c92d2a6818bc60a38e770979", + "format": 1 + }, + { + "name": "tests/integration/targets/get_certificate/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/get_certificate/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e0a51a5256b53303f5ee119bfc550091357a592d42629cb1efc6fb7d4ab5e7ee", + "format": 1 + }, + { + "name": "tests/integration/targets/get_certificate/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/get_certificate/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "02a8e096a0c7f4e1aec96643b37f0cde12a5b081571607223781fdc63bea01c7", + "format": 1 + }, + { + "name": "tests/integration/targets/get_certificate/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/get_certificate/tests/validate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "87594c273be2a7444848e55d5f08e149c8d2bc4792c7a66cc0cfdc3ffa064e16", + "format": 1 + }, + { + "name": "tests/integration/targets/get_certificate/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "50ee7f2486746e5be8a290f79803358a8e1f0da2d84f24bf7d7bd0823f515737", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/files/keyfile1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/files/keyfile1.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/files/keyfile2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "937e8d5fbb48bd4949536cd65b8d35c426b80d2f830c5c308e2cdec422ae2244", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/files/keyfile2.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3b4106020a362e34cbc75011bfa013d65acb8224f6fbc931cfc6438b67888627", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/tasks/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/tasks/tests/create-destroy.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ca17ead6cafa7c149d8f6e08f58f20bf3eb9bbcf8da4d8d804a73f3caa27cbba", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/tasks/tests/device-check.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "33197239094dc49908f0dda9afee398baae4ffc763b9b783f4233bccbdd2780a", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/tasks/tests/key-management.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2f8268b33be4542f9caa9331f64008626998a776cd1d608923dc8a1597e247af", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/tasks/tests/options.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "009eb1c88f09b3be22ad1f66fb6011f3819f7352c7a333e3969920aec415cee8", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/tasks/tests/passphrase.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6690cc4d83157208301ff805f5912e3dd9d447f8b535b151e3a02b701fc794a7", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/tasks/tests/performance.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b007aca0d0d7846e2f78881837c321820bc8d097da767eb23a49cea4e0d86f4f", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ad5a069b9ee7090634fdfaeb9c6090f9c457baff42f4a8b56c68ae478d0d5f8e", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/tasks/run-test.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3d04bbfde05cbc1a3505b96a5dc816048a7d14477a8c1ad6a5ff6d140337b0d9", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/vars/Alpine.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ce279a0328bf225612cf60d52eb0dc0b6efa92cf3e2bd63b0cb528729ad4f997", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/vars/default.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0300999b4fb33af5bcf193812a7b2cc216766d855f3930514a0f059779e1a063", + "format": 1 + }, + { + "name": "tests/integration/targets/luks_device/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8ecccd41c5d56d61c50b07b7f10db10c8724665bb4f0211ef991cbefba0e20d2", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9cad72603bb077120b126b88eebe2847507a90630b8b2571083fac1a0cb03cae", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9832d43453c8072f2803de547f14e7160e2e1dfdd1cde65e4f14d3b6b6f828ad", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert/tests/idempotency.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "91b187a19efb2b8b947b5952ae19c0ffc3a1aa92f9e46e5fa89bd01ec457fd76", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert/tests/key_idempotency.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fd11679be8ac84b2ea91b9e936d05ba4f825fa325e14de76d7696bf7749c1f45", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert/tests/options_idempotency.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "71bc8691ff4f5cf61a3256106e9c34404f0e5b725aa7e7e4062b1faf4b487021", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert/tests/regenerate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "80656692465e2322672e4a9b93131291bc55fdfdbfa7301ac04baed0036b124d", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert/tests/remove.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fb5e2105c08a7cf56fafae98c1a22ee63942374a61338879a4ab68bba4de0a5e", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert/tests/ssh-agent.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2ad71d145cfb5225efe0c20f951dd7142d6aab53e429a87230e8ec34704284e2", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_cert/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e28d467b791779a59439ace7e1092ec06529f0795d4a1b73da989f1687104b21", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a829f0abfbb7b9626705a604dd767add4f1444cca5d6e49a00f6cfac38d4571a", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "42782c8673e00f4740ebcfb449be0facd85139a06d82ec3a9a09b4f68b87da6b", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/tests/core.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "993386edfc0d6e3fc9ac20fa798d626bfdd0e5fc3723ea4704b0688eb4bf8456", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/tests/cryptography_backend.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3a394c9e1e941a9618efdbe5059f2b7899192c1099e8654873f41c3912b063c1", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/tests/invalid.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8530ebc6f630616be11c880211f715b7a77ba19b72562a8aef35fb977053f3b9", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/tests/options.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "032dff93f47779f1c50c7b0570cb7b900eacf9f9b26197fd71c248b7ff8cf769", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/tests/regenerate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6d8981862fb6b79ac8d4001629a59e4e10098b6b8478d4368d1c2cf167eb2d86", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/tests/state.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b7480f3dc1a62ac6b1f4f5330ba9efe3ec18b7960fb53947a21fd457bee7d0cb", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/vars/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2fa23e35cc8db7b44f8a75938dd45c2e6097f8023f2c2e2eb2fc9196109e9758", + "format": 1 + }, + { + "name": "tests/integration/targets/openssh_keypair/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e28d467b791779a59439ace7e1092ec06529f0795d4a1b73da989f1687104b21", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c783499111f505e0644f8402055b90e898d6ac477aea914d6652493945112d62", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a200d0dbfcbf397f6715eadd88bd4a59dd76c3a8310d2fb7cfc9eaedefca61d8", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr/tests/validate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "336e51c024d6a54488f011b879534c8bd11c0113845393da6ca6a0cd13fa5e45", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e550791b9b9cf709c4dbec99b2fd8603263883e7eab8c8d99805c2aa4bcaa8a2", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_info/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "10fb1eb45d71f781382a20264f59cafdeed3e34268e4f9b6db2b0b0c76ad359f", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1da63053bcd2b32819e40ad462b7bc03407a019e636c862d23efa97fae3f3026", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_pipe", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_pipe/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_pipe/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_pipe/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_pipe/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5dac58977155850eb40dabb15d364ed244112498a683e7ce442dfa55634e4787", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_pipe/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0d0b91b34c2354bfe4d2bc1559cfa9648420b8936722915deadd2483e42b5836", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_csr_pipe/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_dhparam", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_dhparam/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_dhparam/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_dhparam/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_dhparam/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "238fdabcc6f9fe387b192bab2b3cc462400d24730ddfe8088783fec533ddf70b", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_dhparam/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2222fca5c51c66f5ccfa175b4f611ae040a9256e62d6f5c220b440c4841e89cc", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_dhparam/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_dhparam/tests/validate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "328cced1def7f669b1523787e98f3def093f6bfee265c7c93f0d2cd7e813cfbe", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_dhparam/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_pkcs12", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_pkcs12/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_pkcs12/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ed4a6e57cd5701ee4043bf84327b3d0b3215936dfbe2bdb405b5564926398619", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_pkcs12/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_pkcs12/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "894c6ae53efa9bf780734a4a627978992ff92a0c1a7513d4474a39c36459d5e2", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_pkcs12/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6fec81beb0862bed86a0eaabf9438438f201f0049c93b3a216dae9a359aef073", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_pkcs12/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_pkcs12/tests/validate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9923cc462e758275600b5e422331bd77f5efc41307bdd402eb94c0ba22851ac4", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_pkcs12/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e5bca32152230f6a0bd39e0434113aaf042181460f6e1b9b29c461fa8ac3050a", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b6539b706274aef3d2b609d608f4aad88fc81d6b2e63f41eb955077ec8d5f23a", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey/tests/validate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5a7c406b6e524b94b182bd5954cd1b5c259a92491beb740216de7db34d955614", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey/vars/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "65523cb8af9d84f6c4f24783530e35b25ce10c9dcea6f98574cf729d3d738681", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_convert", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_convert/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_convert/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_convert/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_convert/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a5fe8d74b9314bb387cc2bacdc1fee7dc3d782d4549ee60fe3a4225660921b55", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_convert/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2400759da00c05fff558ce3e159a30d464aeea50a54e36cfdb5756e1b0660594", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_convert/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e550791b9b9cf709c4dbec99b2fd8603263883e7eab8c8d99805c2aa4bcaa8a2", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_info/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "13c0b0d1c0295de5e19f041dde4458d223ca8cc6592a6a359ef5c10c4c72dfaa", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "656aac578c05f384dad706418939779b3f21612370a00359df2a4cf58b843cb9", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_pipe", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_pipe/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_pipe/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_pipe/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_pipe/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "69115a606e51f57a900b87a3aa07c79b283c5f29f10c067a5c953bb725a05438", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_pipe/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eccadd32022145f4462747e6df8216be49141510af44f7b6a0b1734eebbc809b", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_privatekey_pipe/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e8e108034382b2746739926810ffc1a9ff257d85e73e95ddaedfa1a6cd221179", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d780daa544f7bf469b16fbdb79ac85ed3fced51e8b8e05e3b2cfb1cc19cbab8e", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fd1588c86e9d43a14af903ec5fda7ff058fde6a8009841f1e3569879bfd1c5b6", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey/tests/validate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e0ed98881fe8a218db8e9f24f3c0b32ac85b59a1cff84212a18d96cf8e499c75", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e550791b9b9cf709c4dbec99b2fd8603263883e7eab8c8d99805c2aa4bcaa8a2", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey_info/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8e0cfe534d41332a3759f1378eeeb9f0bf78c8153f5aff6168414aaf03450d71", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a6e588d839c7c7306291c4d01a730e314f1ba33befaefce1a89978a37444597a", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_publickey_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_signature", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_signature/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_signature/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_signature/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_signature/tasks/loop.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c948b76a68f9504eec9ac96f8fe2f6b8222fbb5327914c37bb9da1bef684eced", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_signature/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "aa0d4334a20b8947a5a838c71c3226537c81c7089f48742a4d5bac59210d77cb", + "format": 1 + }, + { + "name": "tests/integration/targets/openssl_signature/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "daf18fe67bd7c12ca28cdcba66db7a11d504158bbbe9cdaa34d775114dde6094", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_http_tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_http_tests/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_http_tests/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1868276efc6d8c8a122911a559296c563833d7dbc58331aa7cc65d1584117b15", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_http_tests/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_http_tests/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3b4106020a362e34cbc75011bfa013d65acb8224f6fbc931cfc6438b67888627", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_http_tests/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_http_tests/tasks/default.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eca1b53842d7571250b25d99a9d871ac8ab7b136b0b61edb899d2d50efc3d5ec", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_http_tests/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0c72dcc7e4dc02176cd57ebc650e4294f34b61d305de6cd4eb94403a73b4cc22", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_http_tests/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_http_tests/vars/httptester.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ff8395ab05e0d79ba203789ae0993f7ba53f26d544cdf96a407afb58f35e3c2b", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_jinja2_compat", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_jinja2_compat/filter_plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_jinja2_compat/filter_plugins/jinja_compatibility.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "30fcb05d83e11b370c30e2e84d8fd9f3661369200ade7dd02df9c002a2309a23", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_jinja2_compat/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_jinja2_compat/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "63a1b79555c097c3c1d8b0bdb2b84b86b91873c6d3d55b7534067c8fea6d41fb", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_jinja2_compat/test_plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_jinja2_compat/test_plugins/jinja_compatibility.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fb6b297710bb3a83b00c9d87c4007500a02d8c3b656a5a3fd70812dc1351833e", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_tests/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_tests/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "63a1b79555c097c3c1d8b0bdb2b84b86b91873c6d3d55b7534067c8fea6d41fb", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_acme", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_acme/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_acme/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_acme/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_acme/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "110cb4c526f60f6aa2cbba3b4edb70bf80e99d3233be368cf4bb41e6b123d494", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_acme/tasks/obtain-cert.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4ad45f93e26e6683cee0b57a66ddabe0147dd5d3af96fadc2209ab4d501b407a", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_bcrypt", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_bcrypt/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_bcrypt/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cb68a78cb264725dfaa8d3647048f08b136626454fb58e349c909e13b19d4be1", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_bcrypt/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_bcrypt/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "90818840327071b8ea826c6a611ff62fcb5b0095a158c865a1ffc5e8ffb4ebed", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ce9310b583ddb7444b5d5c1ed279a7e0ced769bdb32bb3839f74a8beed9804d7", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "15e221e445ae4552bf60cd61a60b9e77c901e4038dad716bde609f2e260cb6d6", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl/vars/Alpine.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6f5ff5e73d8b0936ebc6dd5d12cd29615de400295fc815cfb37369d973e16022", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl/vars/Archlinux.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f5b40e03fa15d3c399bd9c2af432c07e3a58688ffb252ff6c5faa7bf4bbe994e", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl/vars/Debian.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c676cdeab05c15f21a641ba289df70267ebfae211c36661336ce133a8f1b2beb", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl/vars/FreeBSD.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9ed2d263b898271ea2912a52004524ef021342db9245655328d06f5366371773", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl/vars/RedHat.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c676cdeab05c15f21a641ba289df70267ebfae211c36661336ce133a8f1b2beb", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl/vars/Suse.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c676cdeab05c15f21a641ba289df70267ebfae211c36661336ce133a8f1b2beb", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_openssl/vars/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3727ab29e286abd1426ad045ce688bc1bf15cbefba9572d5ac4f5b81adfbfac1", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pkg_mgr", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pkg_mgr/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pkg_mgr/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5e580427bba22d2c11ec743c1fb625ec1efb27f62f4c024bfbd53c3f2ac0c1e5", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a82b1b8b1102ddbd47eb8b9b40a381c8e17ee64d09ec676f3da6505ff1f40fe3", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ce9310b583ddb7444b5d5c1ed279a7e0ced769bdb32bb3839f74a8beed9804d7", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bc7eee77434611615fe83d0207f57480e2587d7cbac729dac3d03da05a64e189", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/vars/Alpine.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "15c974fbfa6e91148b6b991133e464ed5021643aee4e6f8d65a6d0c6e9ab5135", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/vars/Archlinux.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bfd22b490265a8ebb7e7321503a2002d5bdf764aa06f10a53540670d0b8cb37f", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/vars/Debian.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d6b45a3de6991da9377f9177890b4c442c9981ee4bae27a47a3bb61733e77d63", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/vars/FreeBSD.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dd1141ab38968be9ef891f9b158b65773dbf28ad2c92bb9d3d444ab42b3f0c9e", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/vars/RedHat-9.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "053f69bfc45da2725cdbee06f5f8f67c4aec2d136c94e85a2285f5bd4f0adf38", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/vars/RedHat.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4fae8f23afa5273b7558a3cab7ddb920eceffd52e3411db7da59eea858951399", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_pyopenssl/vars/Suse.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f4ecaf1b35ca533787e54dabc5c8380d1d4f0a201f97c78a948affe582edb15a", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_python_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_python_info/filter_plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_python_info/filter_plugins/version_filter.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ad79ba02378296f895d999ae5d3a97eb3de1044c49bc48792ef47e15eb149781", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_python_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_python_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "65a789ea21dc7c4a873935fcb9042c8d4b7a7f8300c98b6546644593411b87cf", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_python_info/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_python_info/vars/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a2ac53510da02da97af29c837c144f05d9a6f3b5863156917fc704d9f6d3434a", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_constraints", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_constraints/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_constraints/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3b4106020a362e34cbc75011bfa013d65acb8224f6fbc931cfc6438b67888627", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_constraints/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_constraints/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bb8e2e8d36b9ecef1534b380c39e8402821718a0bf2246bf896d2222fa381cf0", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_constraints/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "60c77e9e9feaec01032830fa3e185152cb4aacbbf0b17279e6a9c3efb40b4835", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/handlers", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1e57bc20952896b688478c2f21476b5d8310b6430c30e820e940e4734fcde83d", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "54730c80c5fa787eea72dac59c88627036aeae96c48e27cbec02f9c1bd49a2d2", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/tasks/default.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f2453a2b50bc5d163baeb3451bbaab477437831c8b531011b81957f66ad6e890", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "665db3263e44a68e5bd728127a0b1a6dcf93a93eb529e80900a4c4c3fa06d68e", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_agent", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_agent/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_agent/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "19f5a9eb64b2fd1cd8d244d5c50c9525e890866b18943be3df7694c7c5b58bf0", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_agent/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_agent/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b04284c1218aeacf68deb09c78dd26b15787d391cde3b99aada63f7dd77f51d3", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_keygen", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_keygen/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_keygen/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e11ca6edae518cc531ab425daa9eb93f78f89b9ddd515deabd239e8c7925323d", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_keygen/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_keygen/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f9e0dd37154b43cd97290b75d50d9e9cd91ed4ae2b08d000502c464a60d856dd", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_keygen/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_keygen/vars/Alpine.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "997eba54570f24b4cf4d3aad50f4bca07c5c80e4d79aa2cae88df8ee08299b22", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_keygen/vars/Archlinux.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "997eba54570f24b4cf4d3aad50f4bca07c5c80e4d79aa2cae88df8ee08299b22", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_keygen/vars/Debian.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b5c60fe311f37cb59524513c0b9da8f5d1245054fa0101f6e004c292747e0957", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_keygen/vars/RedHat.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "47000e558b1685692e163aa482289c80d138cb4731630e01eb5028a865acc384", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_ssh_keygen/vars/Suse.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "997eba54570f24b4cf4d3aad50f4bca07c5c80e4d79aa2cae88df8ee08299b22", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate-acme", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate-acme/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate-acme/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "933a27a2f645a3ade66cf95034893a8ec058cc68d9c971619937b84923e82c8c", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate-acme/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate-acme/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "73eb2ded345615f54059852adec3416411bf990ed1e35b53dd9b36ba4f5ae774", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate-acme/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "61ff7845af05c774a2c81468d01081bef7ebbc71c56adfcbb00ec97c5320208a", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate-acme/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7332bc60f3a3ac1c9b65b0ae4239443ed0b090be94d93bac1542f55550f6ed63", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "999d1923206cf360540bc7c206bc4070525b378997b953c337ddf27a2b4b8213", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0fcd18a9d2ce5aafd35ab9aecec5963bb56a0483d032993a88ec78e92e154a9e", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate/tasks/ownca.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "318ebf03e19480212efdeb7a1643858fc5d7eb761c414c9de4d234f5fdc24232", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate/tasks/removal.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3821a9e983a3d685d8b2a76069bdede60e33b856f6955adaeda7ec97595afc4e", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate/tasks/selfsigned.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6b085441e03a22f8d7a87b902b0f17d17239982fe53d7522ef8b6ef2e68b8f41", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate/tests/validate_ownca.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9c45d8284903fb862aa9cbb242bdddb6173cc4ed7b03ca55e0241793dfd63b54", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "749ff8a1ce325c0c9b406ec5cc58b21e4c1e66cb1d26c950a88b90140aa11c47", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_info/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_info/files/cert1.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e356f2747f331e35160a278a2d90e619e095cb6beba645692dfd6ed75640d426", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e550791b9b9cf709c4dbec99b2fd8603263883e7eab8c8d99805c2aa4bcaa8a2", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_info/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f8b3f0a4906207c47617632bad13b5d81a22f6a41d5437d37e6a536c9ba2eed4", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8342bd406d6ab4814dfa4b58100f2cd42c139a0ec4b6f4ef4adb1794f263b627", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_pipe", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_pipe/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_pipe/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_pipe/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_pipe/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c20f2e4022bd2ee152e7c04d1e52b0c00bd40a021b35a57b5582acde389fabb9", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_pipe/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3b727d70e1d70466bdb16764bff38c5e7951b1220163d973419f30f38b775981", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_certificate_pipe/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efd331a62ed0b05469a451ee76dfab1231fee154835999a0cda0473e9bfe30f8", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_crl", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_crl/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_crl/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b0dcde99cac9ee9ec8bf29c4a4e4f4bc9e16ebcf0a5834ab22481de66a3e042", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_crl/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_crl/tasks/impl.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3a4100f786b8a06953a0fc3c8d7344670a9da7ba7aaeda0e7d65903966f2697c", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_crl/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "56ba87f12bc6819a667b04746c2fe2cdfe410e179fe8100391ab88c712188ed4", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_crl/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/x509_crl/tests/validate.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f12fc4b8301492fe16444614b0a9f664391f29cd8e3e75b1470c4adccc32e8ad", + "format": 1 + }, + { + "name": "tests/integration/targets/x509_crl/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8ccbe64a76548d88a03ac463624cba59d405bd88027fbd0068833872411513c8", + "format": 1 + }, + { + "name": "tests/integration/requirements.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cea2ce24273ebb7c34f862ed636fc20c000f11434440d004ddd7041259592ce8", + "format": 1 + }, + { + "name": "tests/sanity", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/sanity/extra", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/sanity/extra/extra-docs.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6c7fbc8a07fa803ce062387232eb0d0198a311f5347493f06c19a07aa45a9bf6", + "format": 1 + }, + { + "name": "tests/sanity/extra/extra-docs.json.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/extra/extra-docs.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0fbd87476e9c35e4c5feb31be4aa1e8fc6aebf0de13058e5a267879f741ec0bf", + "format": 1 + }, + { + "name": "tests/sanity/extra/licenses.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8c111eb62fa6f1b6c8a1260e9ff06c8b76ef8244428c6289969d79678093618f", + "format": 1 + }, + { + "name": "tests/sanity/extra/licenses.json.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/extra/licenses.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ec45d81c13e52b6cc1412323ab17415f4435e70f0a5b0ecd1bcb5856753f2f10", + "format": 1 + }, + { + "name": "tests/sanity/extra/licenses.py.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "88f745b5d91e1371369c207e3392877af6f3e1de48fbaca63a728d4dcf79e03c", + "format": 1 + }, + { + "name": "tests/sanity/extra/no-unwanted-files.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a3d3b17f699b042958c7cd845a9d685bc935d83062e0bcf077f2c7200e2c0bac", + "format": 1 + }, + { + "name": "tests/sanity/extra/no-unwanted-files.json.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/extra/no-unwanted-files.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "416fdc4ddd0ad89ddad54bcacc8bf5790d3a9610f7c059167123046849f0ade9", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.10.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e90b8ed0470d877e85848af2108f06b47aef9c8a7e25cae6762ccb8a32ae2214", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.10.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.11.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e90b8ed0470d877e85848af2108f06b47aef9c8a7e25cae6762ccb8a32ae2214", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.11.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.12.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9bdf1ce59dadc7d5e17b1d3b114fe08cebb91648ce3ec46d5c2b69c4d965ce69", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.12.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.13.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7e2345994ac361630efde6d1dcb5657a36a0376d7489731c6fe9d584289402cc", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.13.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.14.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7e2345994ac361630efde6d1dcb5657a36a0376d7489731c6fe9d584289402cc", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.14.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.15.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7e2345994ac361630efde6d1dcb5657a36a0376d7489731c6fe9d584289402cc", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.15.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.16.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7e2345994ac361630efde6d1dcb5657a36a0376d7489731c6fe9d584289402cc", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.16.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.9.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "79b8db85358ed8f96c4cb0159e502a560e52635341a8cc9ee906d46503f8b37d", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.9.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/compat", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/compat/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/compat/builtins.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1a66bf5868ec79d871566ca33e62612c7467681405a2c8aef8a93a768c3deebb", + "format": 1 + }, + { + "name": "tests/unit/compat/mock.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "75bef6f2ce5456591fd26298e8181709215fa31da43c2d25ce70ae73fd6a2936", + "format": 1 + }, + { + "name": "tests/unit/compat/unittest.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9db3b735dd4bde864e6c9d0f18a5a487d336bb4b60fd6b83088d72b4b05a021c", + "format": 1 + }, + { + "name": "tests/unit/plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/cert_1.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "97d36de524ffebd29969c7a1d07353b799f71bbf75fa370c30f37650b1fd2a64", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/cert_1.pem.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "10c425130b2b882fc97f47f647fe0c0f22cb236671ef3728e5184a9f006763ce", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem.old", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "74cfafa46eebe2c36b1d5be784265111787ddae6690c231f4ec8d4e047f435b6", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem.old.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/csr_1.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4444cf7d12202eeffd5b6cc04574b24f61dc302c8ce6c5f81537872d30db233a", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/csr_1.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/csr_2.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "981c99ecb74a45f86f2c6cd2f81a558da472c611b162f86c26638678cec515c4", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/csr_2.pem.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/csr_2.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fb040b8c9d50143e6fb3540bda21d14defeeb0d2ab3354a7d9483a5e025d4b42", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/csr_2.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1d49d28cca5b377cd5e2fa4495455e7c4b616ed5a672bd88bbfef6df573b45a6", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.pem.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "671b19bfc3a726ca1ff99873ac38a9369a36a10b5732322d8626e9ecd94ea2b0", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/backend_data.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9b69baa0e28c3a4dc12e281c326a7f2280e2666fbbd3f51d8b34262b65640c52", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/test_backend_cryptography.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fccabe7cb7879c3189d18a3057cd9768544c6c8f5d9416aac48687b59ee20de5", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "810e5201c70b6e3bc1466d468c5fa822b7e8f5ed381f95de33b386b3a504cab4", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/test_challenges.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7cb2f219f428123556aee4cf25d10f506b546a41b1037278e055e8fae66202ad", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/test_errors.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f05198cadf910ac5a426e9ac9dd45ebd6886d831a0a2a89a4ac2d7293c07d83c", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/test_io.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fed15308c2f651ee4690a164bf29d4c2f6cb2c562658f5e501f78eae5286257e", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/test_orders.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "610b762ab49fb0d3800e0ec246550afcbeadd2940e6b2069ba1ed4ae39c736f0", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/acme/test_utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3658a226ba4a966807e1b4b07b1afb25b38663e95fe2053909fb93eb3339d72e", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/crypto", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/crypto/test_asn1.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "88668057e7ed52b0242412e1c713ad18c407ca51cde0dac0bebdd39e23bc0ebb", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/crypto/test_cryptography_support.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "54b061ad495327d2fc40b77bd04954b644c509b3a339559f260dc548bdf5ae31", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/openssh", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/openssh/test_certificate.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8d81f984ebb04131bfde829d8239ce74633f5116c3ead4f9f2285646a6e986b2", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/openssh/test_cryptography.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c97e6d3736948f53a36c7e1db5587346f69a5fdc844a70c3835f6b4573037583", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/openssh/test_utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0aa67718f63f5825b93e0d12e8833138a4035749088193e6719cb69cdaeea643", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_luks_device.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "359d84e2d64b7c73b30248eded2104aa5121344533354ff453ae506bc4c609ce", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4b89fb58686773c08f1f4b873354e787d447b0a7f821b72eb1afeff394ffd196", + "format": 1 + }, + { + "name": "tests/unit/requirements.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "058e805bb60a0ff1e313811cdd79fbc593c620b7cf9f1af60409e729affb4c93", + "format": 1 + }, + { + "name": "tests/utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/utils/shippable", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/utils/shippable/alpine.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3ae2b6c29dc4ed807ad0389b1b55f11c59ce6015d22c4eec2823c3bb908ed984", + "format": 1 + }, + { + "name": "tests/utils/shippable/fedora.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3ae2b6c29dc4ed807ad0389b1b55f11c59ce6015d22c4eec2823c3bb908ed984", + "format": 1 + }, + { + "name": "tests/utils/shippable/freebsd.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3ae2b6c29dc4ed807ad0389b1b55f11c59ce6015d22c4eec2823c3bb908ed984", + "format": 1 + }, + { + "name": "tests/utils/shippable/macos.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3ae2b6c29dc4ed807ad0389b1b55f11c59ce6015d22c4eec2823c3bb908ed984", + "format": 1 + }, + { + "name": "tests/utils/shippable/osx.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3ae2b6c29dc4ed807ad0389b1b55f11c59ce6015d22c4eec2823c3bb908ed984", + "format": 1 + }, + { + "name": "tests/utils/shippable/rhel.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3ae2b6c29dc4ed807ad0389b1b55f11c59ce6015d22c4eec2823c3bb908ed984", + "format": 1 + }, + { + "name": "tests/utils/shippable/ubuntu.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3ae2b6c29dc4ed807ad0389b1b55f11c59ce6015d22c4eec2823c3bb908ed984", + "format": 1 + }, + { + "name": "tests/utils/shippable/generic.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "06c2cffbba95dcc4c75e33e459ff1b515574e0d9692b289041066b44685b0b0e", + "format": 1 + }, + { + "name": "tests/utils/shippable/linux-community.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a94ce1b7e6ceae1382a8e786545fc0378f0c8eb0c09204da18b1d8e319446850", + "format": 1 + }, + { + "name": "tests/utils/shippable/linux.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "73ac21c284e9957f631402b0465e409033a2f68086764b278e3289b0f03ecf48", + "format": 1 + }, + { + "name": "tests/utils/shippable/remote.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3ae2b6c29dc4ed807ad0389b1b55f11c59ce6015d22c4eec2823c3bb908ed984", + "format": 1 + }, + { + "name": "tests/utils/shippable/sanity.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a5c298630e328b6de875b0db8a48db50592ca493f93004d7975cbfcb119a964f", + "format": 1 + }, + { + "name": "tests/utils/shippable/shippable.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "471b1d343b88039ef089ab494312320d6cdd94fb7b2492b5eee340154ab20027", + "format": 1 + }, + { + "name": "tests/utils/shippable/units.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1bf7f0efe3a1d682ea2aeb0828444324d4a718834210ef8c4cacd0ffe7a681fb", + "format": 1 + }, + { + "name": "tests/utils/constraints.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c201bcb88a212328341ad4d913c8acb61ad6fbb710c17547fa3f6e0868053a82", + "format": 1 + }, + { + "name": "tests/.gitignore", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "58cfc1cc2436abdda80f8c752f3157c649bd38c86379f93c653dc0f7f1deb766", + "format": 1 + }, + { + "name": "tests/config.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "498d46cf08b5abb09880088fa8fd142315f7f775e94cdc7d41364ed20fc2cd65", + "format": 1 + }, + { + "name": "CHANGELOG.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ab26b4a0179855ed65910b80d42b79ba234d9b44a3a5ad445100fac7c9454dae", + "format": 1 + }, + { + "name": "CHANGELOG.rst.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "COPYING", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986", + "format": 1 + }, + { + "name": "README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "31f412268cabb2a6b8f80cbce3b2e30acf973905af3c672155a1b4b6cfef1b0e", + "format": 1 + } + ], + "format": 1 +}
\ No newline at end of file diff --git a/ansible_collections/community/crypto/LICENSES/Apache-2.0.txt b/ansible_collections/community/crypto/LICENSES/Apache-2.0.txt new file mode 100644 index 000000000..62589edd1 --- /dev/null +++ b/ansible_collections/community/crypto/LICENSES/Apache-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ansible_collections/community/crypto/LICENSES/BSD-2-Clause.txt b/ansible_collections/community/crypto/LICENSES/BSD-2-Clause.txt new file mode 100644 index 000000000..6810e04e3 --- /dev/null +++ b/ansible_collections/community/crypto/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,8 @@ +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. + diff --git a/ansible_collections/community/crypto/LICENSES/BSD-3-Clause.txt b/ansible_collections/community/crypto/LICENSES/BSD-3-Clause.txt new file mode 100644 index 000000000..ec1a29d34 --- /dev/null +++ b/ansible_collections/community/crypto/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,27 @@ +Copyright (c) Individual contributors. +All rights reserved. + +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. + + 3. Neither the name of PyCA Cryptography nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +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 OWNER 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. diff --git a/ansible_collections/community/crypto/LICENSES/GPL-3.0-or-later.txt b/ansible_collections/community/crypto/LICENSES/GPL-3.0-or-later.txt new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/ansible_collections/community/crypto/LICENSES/GPL-3.0-or-later.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/ansible_collections/community/crypto/LICENSES/PSF-2.0.txt b/ansible_collections/community/crypto/LICENSES/PSF-2.0.txt new file mode 100644 index 000000000..35acd7fb5 --- /dev/null +++ b/ansible_collections/community/crypto/LICENSES/PSF-2.0.txt @@ -0,0 +1,48 @@ +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. diff --git a/ansible_collections/community/crypto/MANIFEST.json b/ansible_collections/community/crypto/MANIFEST.json new file mode 100644 index 000000000..cd7e04ed8 --- /dev/null +++ b/ansible_collections/community/crypto/MANIFEST.json @@ -0,0 +1,48 @@ +{ + "collection_info": { + "namespace": "community", + "name": "crypto", + "version": "2.14.0", + "authors": [ + "Ansible (github.com/ansible)" + ], + "readme": "README.md", + "tags": [ + "acme", + "certificate", + "community", + "crl", + "cryptography", + "csr", + "dhparam", + "entrust", + "letsencrypt", + "luks", + "openssl", + "openssh", + "pkcs12" + ], + "description": null, + "license": [ + "GPL-3.0-or-later", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "PSF-2.0" + ], + "license_file": null, + "dependencies": {}, + "repository": "https://github.com/ansible-collections/community.crypto", + "documentation": "https://docs.ansible.com/ansible/latest/collections/community/crypto/", + "homepage": "https://github.com/ansible-collections/community.crypto", + "issues": "https://github.com/ansible-collections/community.crypto/issues" + }, + "file_manifest_file": { + "name": "FILES.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b1f04aa94c0ac2a75ae7c56654a6e2bfb418a3660a310d3468f119d988ed04f9", + "format": 1 + }, + "format": 1 +}
\ No newline at end of file diff --git a/ansible_collections/community/crypto/README.md b/ansible_collections/community/crypto/README.md new file mode 100644 index 000000000..d0ae6489f --- /dev/null +++ b/ansible_collections/community/crypto/README.md @@ -0,0 +1,134 @@ +<!-- +Copyright (c) Ansible Project +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +--> + +# Ansible Community Crypto Collection + +[![Build Status](https://dev.azure.com/ansible/community.crypto/_apis/build/status/CI?branchName=main)](https://dev.azure.com/ansible/community.crypto/_build?definitionId=21) +[![EOL CI](https://github.com/ansible-collections/community.crypto/workflows/EOL%20CI/badge.svg?event=push)](https://github.com/ansible-collections/community.crypto/actions) +[![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.crypto)](https://codecov.io/gh/ansible-collections/community.crypto) + +Provides modules for [Ansible](https://www.ansible.com/community) for various cryptographic operations. + +You can find [documentation for this collection on the Ansible docs site](https://docs.ansible.com/ansible/latest/collections/community/crypto/). + +Please note that this collection does **not** support Windows targets. + +## Tested with Ansible + +Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12, ansible-core 2.13, and ansible-core 2.14 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported. + +## External requirements + +The exact requirements for every module are listed in the module documentation. + +Most modules require a recent enough version of [the Python cryptography library](https://pypi.org/project/cryptography/). See the module documentations for the minimal version supported for each module. + +## Collection Documentation + +Browsing the [**latest** collection documentation](https://docs.ansible.com/ansible/latest/collections/community/crypto) will show docs for the _latest version released in the Ansible package_, not the latest version of the collection released on Galaxy. + +Browsing the [**devel** collection documentation](https://docs.ansible.com/ansible/devel/collections/community/crypto) shows docs for the _latest version released on Galaxy_. + +We also separately publish [**latest commit** collection documentation](https://ansible-collections.github.io/community.crypto/branch/main/) which shows docs for the _latest commit in the `main` branch_. + +If you use the Ansible package and do not update collections independently, use **latest**. If you install or update this collection directly from Galaxy, use **devel**. If you are looking to contribute, use **latest commit**. + +## Included content + +- OpenSSL / PKI modules: + - openssl_csr_info + - openssl_csr + - openssl_dhparam + - openssl_pkcs12 + - openssl_privatekey_info + - openssl_privatekey + - openssl_publickey + - openssl_signature_info + - openssl_signature + - x509_certificate_info + - x509_certificate + - x509_crl_info + - x509_crl + - certificate_complete_chain +- OpenSSH modules: + - openssh_cert + - openssh_keypair +- ACME modules: + - acme_account_info + - acme_account + - acme_certificate + - acme_certificate_revoke + - acme_challenge_cert_helper + - acme_inspect +- ECS modules: + - ecs_certificate + - ecs_domain +- Miscellaneous modules: + - get_certificate + - luks_device + +You can also find a list of all modules with documentation on the [Ansible docs site](https://docs.ansible.com/ansible/latest/collections/community/crypto/). + +## Using this collection + +Before using the crypto community collection, you need to install the collection with the `ansible-galaxy` CLI: + + ansible-galaxy collection install community.crypto + +You can also include it in a `requirements.yml` file and install it via `ansible-galaxy collection install -r requirements.yml` using the format: + +```yaml +collections: +- name: community.crypto +``` + +See [Ansible Using collections](https://docs.ansible.com/ansible/latest/user_guide/collections_using.html) for more details. + +## Contributing to this collection + +<!--Describe how the community can contribute to your collection. At a minimum, include how and where users can create issues to report problems or request features for this collection. List contribution requirements, including preferred workflows and necessary testing, so you can benefit from community PRs. If you are following general Ansible contributor guidelines, you can link to - [Ansible Community Guide](https://docs.ansible.com/ansible/latest/community/index.html). --> + +We're following the general Ansible contributor guidelines; see [Ansible Community Guide](https://docs.ansible.com/ansible/latest/community/index.html). + +If you want to clone this repositority (or a fork of it) to improve it, you can proceed as follows: +1. Create a directory `ansible_collections/community`; +2. In there, checkout this repository (or a fork) as `crypto`; +3. Add the directory containing `ansible_collections` to your [ANSIBLE_COLLECTIONS_PATH](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#collections-paths). + +See [Ansible's dev guide](https://docs.ansible.com/ansible/devel/dev_guide/developing_collections.html#contributing-to-collections) for more information. + +## Release notes + +See the [changelog](https://github.com/ansible-collections/community.crypto/blob/main/CHANGELOG.rst). + +## Roadmap + +We plan to regularly release minor and patch versions, whenever new features are added or bugs fixed. Our collection follows [semantic versioning](https://semver.org/), so breaking changes will only happen in major releases. + +Most modules will drop PyOpenSSL support in version 2.0.0 of the collection, i.e. in the next major version. We currently plan to release 2.0.0 somewhen during 2021. Around then, the supported versions of the most common distributions will contain a new enough version of ``cryptography``. + +Once 2.0.0 has been released, bugfixes will still be backported to 1.0.0 for some time, and some features might also be backported. If we do not want to backport something ourselves because we think it is not worth the effort, backport PRs by non-maintainers are usually accepted. + +In 2.0.0, the following notable features will be removed: +* PyOpenSSL backends of all modules, except ``openssl_pkcs12`` which does not have a ``cryptography`` backend due to lack of support of PKCS#12 functionality in ``cryptography``. +* The ``assertonly`` provider of ``x509_certificate`` will be removed. + +## More information + +- [Ansible Collection overview](https://github.com/ansible-collections/overview) +- [Ansible User guide](https://docs.ansible.com/ansible/latest/user_guide/index.html) +- [Ansible Developer guide](https://docs.ansible.com/ansible/latest/dev_guide/index.html) +- [Ansible Community code of conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) + +## Licensing + +This collection is primarily licensed and distributed as a whole under the GNU General Public License v3.0 or later. + +See [LICENSES/GPL-3.0-or-later.txt](https://github.com/ansible-collections/community.crypto/blob/main/COPYING) for the full text. + +Parts of the collection are licensed under the [Apache 2.0 license](https://github.com/ansible-collections/community.crypto/blob/main/LICENSES/Apache-2.0.txt) (`plugins/module_utils/crypto/_obj2txt.py` and `plugins/module_utils/crypto/_objects_data.py`), the [BSD 2-Clause license](https://github.com/ansible-collections/community.crypto/blob/main/LICENSES/BSD-2-Clause.txt) (`plugins/module_utils/ecs/api.py`), the [BSD 3-Clause license](https://github.com/ansible-collections/community.crypto/blob/main/LICENSES/BSD-3-Clause.txt) (`plugins/module_utils/crypto/_obj2txt.py`), and the [PSF 2.0 license](https://github.com/ansible-collections/community.crypto/blob/main/LICENSES/PSF-2.0.txt) (`plugins/module_utils/_version.py`). This only applies to vendored files in ``plugins/module_utils/`` and to the ECS module utils. + +Almost all files have a machine readable `SDPX-License-Identifier:` comment denoting its respective license(s) or an equivalent entry in an accompanying `.license` file. Only changelog fragments (which will not be part of a release) are covered by a blanket statement in `.reuse/dep5`. Right now a few vendored PEM files do not have licensing information as well. This conforms to the [REUSE specification](https://reuse.software/spec/) up to the aforementioned PEM files. diff --git a/ansible_collections/community/crypto/changelogs/changelog.yaml b/ansible_collections/community/crypto/changelogs/changelog.yaml new file mode 100644 index 000000000..ca735b395 --- /dev/null +++ b/ansible_collections/community/crypto/changelogs/changelog.yaml @@ -0,0 +1,1147 @@ +ancestor: null +releases: + 1.0.0: + changes: + bugfixes: + - 'ACME modules: fix bug in ACME v1 account update code' + - 'ACME modules: make sure some connection errors are handled properly' + - 'ACME modules: support Buypass'' ACME v1 endpoint' + - acme_certificate - fix crash when module is used with Python 2.x. + - acme_certificate - fix misbehavior when ACME v1 is used with ``modify_account`` + set to ``false``. + - 'ecs_certificate - Always specify header ``connection: keep-alive`` for ECS + API connections.' + - ecs_certificate - Fix formatting of contents of ``full_chain_path``. + - get_certificate - Fix cryptography backend when pyopenssl is unavailable (https://github.com/ansible/ansible/issues/67900) + - openssh_keypair - add logic to avoid breaking password protected keys. + - openssh_keypair - fixes idempotence issue with public key (https://github.com/ansible/ansible/issues/64969). + - openssh_keypair - public key's file attributes (permissions, owner, group, + etc.) are now set to the same values as the private key. + - openssl_* modules - prevent crash on fingerprint determination in FIPS mode + (https://github.com/ansible/ansible/issues/67213). + - 'openssl_certificate - When provider is ``entrust``, use a ``connection: keep-alive`` + header for ECS API connections.' + - openssl_certificate - ``provider`` option was documented as required, but + it was not checked whether it was provided. It is now only required when ``state`` + is ``present``. + - openssl_certificate - fix ``assertonly`` provider certificate verification, + causing 'private key mismatch' and 'subject mismatch' errors. + - openssl_certificate and openssl_csr - fix Ed25519 and Ed448 private key support + for ``cryptography`` backend. This probably needs at least cryptography 2.8, + since older versions have problems with signing certificates or CSRs with + such keys. (https://github.com/ansible/ansible/issues/59039, PR https://github.com/ansible/ansible/pull/63984) + - openssl_csr - a warning is issued if an unsupported value for ``version`` + is used for the ``cryptography`` backend. + - openssl_csr - the module will now enforce that ``privatekey_path`` is specified + when ``state=present``. + - openssl_publickey - fix a module crash caused when pyOpenSSL is not installed + (https://github.com/ansible/ansible/issues/67035). + deprecated_features: + - openssl_csr - all values for the ``version`` option except ``1`` are deprecated. + The value 1 denotes the current only standardized CSR version. + minor_changes: + - luks_device - accept ``passphrase``, ``new_passphrase`` and ``remove_passphrase``. + - luks_device - add ``keysize`` parameter to set key size at LUKS container + creation + - luks_device - added support to use UUIDs, and labels with LUKS2 containers + - luks_device - added the ``type`` option that allows user explicit define the + LUKS container format version + - openssh_keypair - instead of regenerating some broken or password protected + keys, fail the module. Keys can still be regenerated by calling the module + with ``force=yes``. + - openssh_keypair - the ``regenerate`` option allows to configure the module's + behavior when it should or needs to regenerate private keys. + - openssl_* modules - the cryptography backend now properly supports ``dirName``, + ``otherName`` and ``RID`` (Registered ID) names. + - openssl_certificate - Add option for changing which ACME directory to use + with acme-tiny. Set the default ACME directory to Let's Encrypt instead of + using acme-tiny's default. (acme-tiny also uses Let's Encrypt at the time + being, so no action should be neccessary.) + - openssl_certificate - Change the required version of acme-tiny to >= 4.0.0 + - openssl_certificate - allow to provide content of some input files via the + ``csr_content``, ``privatekey_content``, ``ownca_privatekey_content`` and + ``ownca_content`` options. + - openssl_certificate - allow to return the existing/generated certificate directly + as ``certificate`` by setting ``return_content`` to ``yes``. + - openssl_certificate_info - allow to provide certificate content via ``content`` + option (https://github.com/ansible/ansible/issues/64776). + - openssl_csr - Add support for specifying the SAN ``otherName`` value in the + OpenSSL ASN.1 UTF8 string format, ``otherName:<OID>;UTF8:string value``. + - openssl_csr - allow to provide private key content via ``private_key_content`` + option. + - openssl_csr - allow to return the existing/generated CSR directly as ``csr`` + by setting ``return_content`` to ``yes``. + - openssl_csr_info - allow to provide CSR content via ``content`` option. + - openssl_dhparam - allow to return the existing/generated DH params directly + as ``dhparams`` by setting ``return_content`` to ``yes``. + - openssl_dhparam - now supports a ``cryptography``-based backend. Auto-detection + can be overwritten with the ``select_crypto_backend`` option. + - openssl_pkcs12 - allow to return the existing/generated PKCS#12 directly as + ``pkcs12`` by setting ``return_content`` to ``yes``. + - openssl_privatekey - add ``format`` and ``format_mismatch`` options. + - openssl_privatekey - allow to return the existing/generated private key directly + as ``privatekey`` by setting ``return_content`` to ``yes``. + - openssl_privatekey - the ``regenerate`` option allows to configure the module's + behavior when it should or needs to regenerate private keys. + - openssl_privatekey_info - allow to provide private key content via ``content`` + option. + - openssl_publickey - allow to provide private key content via ``private_key_content`` + option. + - openssl_publickey - allow to return the existing/generated public key directly + as ``publickey`` by setting ``return_content`` to ``yes``. + release_summary: 'This is the first proper release of the ``community.crypto`` + collection. This changelog contains all changes to the modules in this collection + that were added after the release of Ansible 2.9.0. + + ' + removed_features: + - The ``letsencrypt`` module has been removed. Use ``acme_certificate`` instead. + fragments: + - 1.0.0.yml + - 52408-luks-device.yaml + - 58973-luks_device_add-type-option.yml + - 58973_luks_device-add-label-and-uuid-support.yml + - 60388-openssl_privatekey-format.yml + - 61522-luks-device-add-option-to-define-keysize.yml + - 61658-openssh_keypair-public-key-permissions.yml + - 61693-acme-buypass-acme-v1.yml + - 61738-ecs-certificate-invalid-chain.yaml + - 62218-fix-to-entrust-api.yml + - 62790-openssl_certificate_fix_assert.yml + - 62991-openssl_dhparam-cryptography-backend.yml + - 63140-acme-fix-fetch-url-status-codes.yaml + - 63432-openssl_csr-version.yml + - 63984-openssl-ed25519-ed448.yml + - 64436-openssh_keypair-add-password-protected-key-check.yml + - 64501-fix-python2.x-backward-compatibility.yaml + - 64648-acme_certificate-acmev1.yml + - 65017-openssh_keypair-idempotence.yml + - 65400-openssl-output.yml + - 65435-openssl_csr-privatekey_path-required.yml + - 65633-crypto-argspec-fixup.yml + - 66384-openssl-content.yml + - 67036-openssl_publickey-backend.yml + - 67038-openssl-openssh-key-regenerate.yml + - 67109-openssl_certificate-acme-directory.yaml + - 67515-openssl-fingerprint-fips.yml + - 67669-cryptography-names.yml + - 67901-get_certificate-fix-cryptography.yml + - letsencrypt.yml + - openssl_csr-otherName.yml + modules: + - description: Request validation of a domain with the Entrust Certificate Services + (ECS) API + name: ecs_domain + namespace: '' + - description: Generate Certificate Revocation Lists (CRLs) + name: x509_crl + namespace: '' + - description: Retrieve information on Certificate Revocation Lists (CRLs) + name: x509_crl_info + namespace: '' + release_date: '2020-07-03' + 1.1.0: + changes: + bugfixes: + - acme_inspect - fix problem with Python 3.5 that JSON was not decoded (https://github.com/ansible-collections/community.crypto/issues/86). + - get_certificate - fix ``ca_cert`` option handling when ``proxy_host`` is used + (https://github.com/ansible-collections/community.crypto/pull/84). + - openssl_*, x509_* modules - fix handling of general names which refer to IP + networks and not IP addresses (https://github.com/ansible-collections/community.crypto/pull/92). + minor_changes: + - acme_account - add ``external_account_binding`` option to allow creation of + ACME accounts with External Account Binding (https://github.com/ansible-collections/community.crypto/issues/89). + - 'acme_certificate - allow new selector ``test_certificates: first`` for ``select_chain`` + parameter (https://github.com/ansible-collections/community.crypto/pull/102).' + - cryptography backends - support arbitrary dotted OIDs (https://github.com/ansible-collections/community.crypto/issues/39). + - get_certificate - add support for SNI (https://github.com/ansible-collections/community.crypto/issues/69). + - luks_device - add support for encryption options on container creation (https://github.com/ansible-collections/community.crypto/pull/97). + - openssh_cert - add support for PKCS#11 tokens (https://github.com/ansible-collections/community.crypto/pull/95). + - openssl_certificate - the PyOpenSSL backend now uses 160 bits of randomness + for serial numbers, instead of a random number between 1000 and 99999. Please + note that this is not a high quality random number (https://github.com/ansible-collections/community.crypto/issues/76). + - openssl_csr - add support for name constraints extension (https://github.com/ansible-collections/community.crypto/issues/46). + - openssl_csr_info - add support for name constraints extension (https://github.com/ansible-collections/community.crypto/issues/46). + release_summary: 'Release for Ansible 2.10.0. + + ' + fragments: + - 1.1.0.yml + - 100-acme-account-external-account-binding.yml + - 102-acme-certificate-select-chain-first.yml + - 87-acme_inspect-python-3.5.yml + - 90-cryptography-oids.yml + - 90-openssl_certificate-pyopenssl-serial.yml + - 92-ip-networks.yml + - 92-openssl_csr-name-constraints.yml + - get_certificate-add_support_for_SNI.yml + - luks_device-add_encryption_option_on_create.yml + - openssh_cert-pkcs11.yml + modules: + - description: Sign data with openssl + name: openssl_signature + namespace: '' + - description: Verify signatures with openssl + name: openssl_signature_info + namespace: '' + release_date: '2020-08-18' + 1.1.1: + changes: + bugfixes: + - meta/runtime.yml - convert Ansible version numbers for old names of modules + to collection version numbers (https://github.com/ansible-collections/community.crypto/pull/108). + - openssl_csr - improve handling of IDNA errors (https://github.com/ansible-collections/community.crypto/issues/105). + release_summary: Bugfixes for Ansible 2.10.0. + fragments: + - 1.1.1.yml + - 106-openssl_csr-idna-errors.yml + - 108-meta-runtime-versions.yml + release_date: '2020-09-14' + 1.2.0: + changes: + bugfixes: + - openssl_pkcs12 - do not crash when reading PKCS#12 file which has no private + key and/or no main certificate (https://github.com/ansible-collections/community.crypto/issues/103). + minor_changes: + - acme_certificate - allow to pass CSR file as content with new option ``csr_content`` + (https://github.com/ansible-collections/community.crypto/pull/115). + - x509_certificate_info - add ``fingerprints`` return value which returns certificate + fingerprints (https://github.com/ansible-collections/community.crypto/pull/121). + release_summary: Please note that this release fixes a security issue (CVE-2020-25646). + security_fixes: + - openssl_csr - the option ``privatekey_content`` was not marked as ``no_log``, + resulting in it being dumped into the system log by default, and returned + in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125). + - openssl_privatekey_info - the option ``content`` was not marked as ``no_log``, + resulting in it being dumped into the system log by default, and returned + in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125). + - openssl_publickey - the option ``privatekey_content`` was not marked as ``no_log``, + resulting in it being dumped into the system log by default, and returned + in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125). + - openssl_signature - the option ``privatekey_content`` was not marked as ``no_log``, + resulting in it being dumped into the system log by default, and returned + in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125). + - x509_certificate - the options ``privatekey_content`` and ``ownca_privatekey_content`` + were not marked as ``no_log``, resulting in it being dumped into the system + log by default, and returned in the registered results in the ``invocation`` + field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125). + - x509_crl - the option ``privatekey_content`` was not marked as ``no_log``, + resulting in it being dumped into the system log by default, and returned + in the registered results in the ``invocation`` field (CVE-2020-25646, https://github.com/ansible-collections/community.crypto/pull/125). + fragments: + - 1.2.0.yml + - 109-openssl_pkcs12-crash-no-cert-key.yml + - 115-acme_certificate-csr_content.yml + - 121-x509_certificate_info-fingerprints.yml + - cve-2020-25646.yml + release_date: '2020-10-13' + 1.3.0: + changes: + bugfixes: + - openssl_pkcs12 - report the correct state when ``action`` is ``parse`` (https://github.com/ansible-collections/community.crypto/issues/143). + - support code - improve handling of certificate and certificate signing request + (CSR) loading with the ``cryptography`` backend when errors occur (https://github.com/ansible-collections/community.crypto/issues/138, + https://github.com/ansible-collections/community.crypto/pull/139). + - x509_certificate - fix ``entrust`` provider, which was broken since community.crypto + 0.1.0 due to a feature added before the collection move (https://github.com/ansible-collections/community.crypto/pull/135). + minor_changes: + - openssh_cert - add module parameter ``use_agent`` to enable using signing + keys stored in ssh-agent (https://github.com/ansible-collections/community.crypto/issues/116). + - openssl_csr - refactor module to allow code re-use by openssl_csr_pipe (https://github.com/ansible-collections/community.crypto/pull/123). + - openssl_privatekey - refactor module to allow code re-use by openssl_privatekey_pipe + (https://github.com/ansible-collections/community.crypto/pull/119). + - openssl_privatekey - the elliptic curve ``secp192r1`` now triggers a security + warning. Elliptic curves of at least 224 bits should be used for new keys; + see `here <https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec.html#elliptic-curves>`_ + (https://github.com/ansible-collections/community.crypto/pull/132). + - x509_certificate - for the ``selfsigned`` provider, a CSR is not required + anymore. If no CSR is provided, the module behaves as if a minimal CSR which + only contains the public key has been provided (https://github.com/ansible-collections/community.crypto/issues/32, + https://github.com/ansible-collections/community.crypto/pull/129). + - x509_certificate - refactor module to allow code re-use by x509_certificate_pipe + (https://github.com/ansible-collections/community.crypto/pull/135). + release_summary: 'Contains new modules ``openssl_privatekey_pipe``, ``openssl_csr_pipe`` + and ``x509_certificate_pipe`` which allow to create or update private keys, + CSRs and X.509 certificates without having to write them to disk. + + ' + fragments: + - 1.3.0.yml + - 117-openssh_cert-use-ssh-agent.yml + - 129-x509_certificate-no-csr-selfsigned.yml + - 132-openssl_privatekey-ecc-order.yml + - 135-x509_certificate-entrust.yml + - 139-improve-error-handling.yml + - 145-add-check-for-parsed-pkcs12-files.yml + - privatekey-csr-certificate-refactoring.yml + modules: + - description: Generate OpenSSL Certificate Signing Request (CSR) + name: openssl_csr_pipe + namespace: '' + - description: Generate OpenSSL private keys without disk access + name: openssl_privatekey_pipe + namespace: '' + - description: Generate and/or check OpenSSL certificates + name: x509_certificate_pipe + namespace: '' + release_date: '2020-11-24' + 1.4.0: + changes: + bugfixes: + - acme_certificate - error when requested challenge type is not found for non-valid + challenges, instead of hanging on step 2 (https://github.com/ansible-collections/community.crypto/issues/171, + https://github.com/ansible-collections/community.crypto/pull/173). + minor_changes: + - The ACME module_utils has been relicensed back from the Simplified BSD License + (https://opensource.org/licenses/BSD-2-Clause) to the GPLv3+ (same license + used by most other code in this collection). This undoes a licensing change + when the original GPLv3+ licensed code was moved to module_utils in https://github.com/ansible/ansible/pull/40697 + (https://github.com/ansible-collections/community.crypto/pull/165). + - The ``crypto/identify.py`` module_utils has been renamed to ``crypto/pem.py`` + (https://github.com/ansible-collections/community.crypto/pull/166). + - luks_device - ``new_keyfile``, ``new_passphrase``, ``remove_keyfile`` and + ``remove_passphrase`` are now idempotent (https://github.com/ansible-collections/community.crypto/issues/19, + https://github.com/ansible-collections/community.crypto/pull/168). + - luks_device - allow to configure PBKDF (https://github.com/ansible-collections/community.crypto/pull/163). + - openssl_csr, openssl_csr_pipe - allow to specify CRL distribution endpoints + with ``crl_distribution_points`` (https://github.com/ansible-collections/community.crypto/issues/147, + https://github.com/ansible-collections/community.crypto/pull/167). + - openssl_pkcs12 - allow to specify certificate bundles in ``other_certificates`` + by using new option ``other_certificates_parse_all`` (https://github.com/ansible-collections/community.crypto/issues/149, + https://github.com/ansible-collections/community.crypto/pull/166). + release_summary: Release with several new features and bugfixes. + fragments: + - 1.4.0.yml + - 163-luks-pbkdf.yml + - 166-openssl_pkcs12-certificate-bundles.yml + - 167-openssl_csr-crl-distribution-points.yml + - 168-luks_device-add-remove-idempotence.yml + - 173-acme_certificate-wrong-challenge.yml + - acme-module-utils-relicense.yml + release_date: '2021-01-26' + 1.5.0: + changes: + bugfixes: + - openssl_csr - no longer fails when comparing CSR without basic constraint + when ``basic_constraints`` is specified (https://github.com/ansible-collections/community.crypto/issues/179, + https://github.com/ansible-collections/community.crypto/pull/180). + deprecated_features: + - acme_account_info - when ``retrieve_orders=url_list``, ``orders`` will no + longer be returned in community.crypto 2.0.0. Use ``order_uris`` instead (https://github.com/ansible-collections/community.crypto/pull/178). + minor_changes: + - acme_account_info - when ``retrieve_orders`` is not ``ignore`` and the ACME + server allows to query orders, the new return value ``order_uris`` is always + populated with a list of URIs (https://github.com/ansible-collections/community.crypto/pull/178). + - luks_device - allow to specify sector size for LUKS2 containers with new ``sector_size`` + parameter (https://github.com/ansible-collections/community.crypto/pull/193). + release_summary: Regular feature and bugfix release. Deprecates a return value. + fragments: + - 1.5.0.yml + - 178-acme_account_info-orders-urls.yml + - 179-openssl-csr-basic-constraint.yml + - 193-luks_device-sector_size.yml + release_date: '2021-03-08' + 1.6.0: + changes: + bugfixes: + - action_module plugin helper - make compatible with latest changes in ansible-core + 2.11.0b3 (https://github.com/ansible-collections/community.crypto/pull/202). + - openssl_privatekey_pipe - make compatible with latest changes in ansible-core + 2.11.0b3 (https://github.com/ansible-collections/community.crypto/pull/202). + deprecated_features: + - acme module_utils - the ``acme`` module_utils (``ansible_collections.community.crypto.plugins.module_utils.acme``) + is deprecated and will be removed in community.crypto 2.0.0. Use the new Python + modules in the ``acme`` package instead (``ansible_collections.community.crypto.plugins.module_utils.acme.xxx``) + (https://github.com/ansible-collections/community.crypto/pull/184). + minor_changes: + - acme module_utils - the ``acme`` module_utils has been split up into several + Python modules (https://github.com/ansible-collections/community.crypto/pull/184). + - acme_* modules - codebase refactor which should not be visible to end-users + (https://github.com/ansible-collections/community.crypto/pull/184). + - acme_* modules - support account key passphrases for ``cryptography`` backend + (https://github.com/ansible-collections/community.crypto/issues/197, https://github.com/ansible-collections/community.crypto/pull/207). + - acme_certificate_revoke - support revoking by private keys that are passphrase + protected for ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/pull/207). + - acme_challenge_cert_helper - add ``private_key_passphrase`` parameter (https://github.com/ansible-collections/community.crypto/pull/207). + release_summary: Fixes compatibility issues with the latest ansible-core 2.11 + beta, and contains a lot of internal refactoring for the ACME modules and + support for private key passphrases for them. + fragments: + - 1.6.0.yml + - 184-acme-refactor.yml + - 202-actionmodule-plugin-utils-ansible-core-2.11.yml + - 207-acme-account-key-passphrase.yml + release_date: '2021-03-22' + 1.6.1: + changes: + bugfixes: + - acme_* modules - fix wrong usages of ``ACMEProtocolException`` (https://github.com/ansible-collections/community.crypto/pull/216, + https://github.com/ansible-collections/community.crypto/pull/217). + release_summary: Bugfix release. + fragments: + - 1.6.1.yml + - 217-acme-exceptions.yml + release_date: '2021-04-11' + 1.6.2: + changes: + bugfixes: + - acme_* modules - avoid crashing for ACME servers where the ``meta`` directory + key is not present (https://github.com/ansible-collections/community.crypto/issues/220, + https://github.com/ansible-collections/community.crypto/pull/221). + release_summary: Bugfix release. Fixes compatibility issue of ACME modules with + step-ca. + fragments: + - 1.6.2.yml + - 221-acme-meta.yml + release_date: '2021-04-28' + 1.7.0: + changes: + bugfixes: + - openssh_keypair - fix ``check_mode`` to populate return values for existing + keypairs (https://github.com/ansible-collections/community.crypto/issues/113, + https://github.com/ansible-collections/community.crypto/pull/230). + - various modules - prevent crashes when modules try to set attributes on not + yet existing files in check mode. This will be fixed in ansible-core 2.12, + but it is not backported to every Ansible version we support (https://github.com/ansible-collections/community.crypto/issue/242, + https://github.com/ansible-collections/community.crypto/pull/243). + - x509_certificate - fix crash when ``assertonly`` provider is used and some + error conditions should be reported (https://github.com/ansible-collections/community.crypto/issues/240, + https://github.com/ansible-collections/community.crypto/pull/241). + minor_changes: + - cryptography_openssh module utils - new module_utils for managing asymmetric + keypairs and OpenSSH formatted/encoded asymmetric keypairs (https://github.com/ansible-collections/community.crypto/pull/213). + - openssh_keypair - added ``backend`` parameter for selecting between the cryptography + library or the OpenSSH binary for the execution of actions performed by ``openssh_keypair`` + (https://github.com/ansible-collections/community.crypto/pull/236). + - openssh_keypair - added ``passphrase`` parameter for encrypting/decrypting + OpenSSH private keys (https://github.com/ansible-collections/community.crypto/pull/225). + - openssl_csr - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, + https://github.com/ansible-collections/community.crypto/pull/150). + - openssl_csr_info - now returns ``public_key_type`` and ``public_key_data`` + (https://github.com/ansible-collections/community.crypto/pull/233). + - openssl_csr_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/204). + - openssl_csr_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, + https://github.com/ansible-collections/community.crypto/pull/150). + - openssl_pkcs12 - added option ``select_crypto_backend`` and a ``cryptography`` + backend. This requires cryptography 3.0 or newer, and does not support the + ``iter_size`` and ``maciter_size`` options (https://github.com/ansible-collections/community.crypto/pull/234). + - openssl_privatekey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, + https://github.com/ansible-collections/community.crypto/pull/150). + - openssl_privatekey_info - refactor module to allow code re-use for diff mode + (https://github.com/ansible-collections/community.crypto/pull/205). + - openssl_privatekey_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, + https://github.com/ansible-collections/community.crypto/pull/150). + - openssl_publickey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, + https://github.com/ansible-collections/community.crypto/pull/150). + - x509_certificate - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, + https://github.com/ansible-collections/community.crypto/pull/150). + - x509_certificate_info - now returns ``public_key_type`` and ``public_key_data`` + (https://github.com/ansible-collections/community.crypto/pull/233). + - x509_certificate_info - refactor module to allow code re-use for diff mode + (https://github.com/ansible-collections/community.crypto/pull/206). + - x509_certificate_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, + https://github.com/ansible-collections/community.crypto/pull/150). + - x509_crl - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, + https://github.com/ansible-collections/community.crypto/pull/150). + - x509_crl_info - add ``list_revoked_certificates`` option to avoid enumerating + all revoked certificates (https://github.com/ansible-collections/community.crypto/pull/232). + - x509_crl_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/203). + release_summary: Regular feature and bugfix release. + fragments: + - 1.7.0.yml + - 150-diff.yml + - 203-x509_crl_info.yml + - 204-openssl_csr_info.yml + - 205-openssl_privatekey_info.yml + - 206-x509_certificate_info.yml + - 213-cryptography-openssh-module-utils.yml + - 225-openssh-keypair-passphrase.yml + - 230-openssh_keypair-check_mode-return-values.yml + - 232-x509_crl_info-list_revoked_certificates.yml + - 233-public-key-info.yml + - 234-openssl_pkcs12-cryptography.yml + - 236-openssh_keypair-backends.yml + - 241-x509_certificate-assertonly.yml + - 243-permission-check-crash.yml + modules: + - description: Provide information for OpenSSL public keys + name: openssl_publickey_info + namespace: '' + release_date: '2021-06-02' + 1.7.1: + changes: + bugfixes: + - openssl_pkcs12 - fix crash when loading passphrase-protected PKCS#12 files + with ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/issues/247, + https://github.com/ansible-collections/community.crypto/pull/248). + release_summary: Bugfix release. + fragments: + - 1.7.1.yml + - 248-openssl_pkcs12-passphrase-fix.yml + release_date: '2021-06-11' + 1.8.0: + changes: + bugfixes: + - openssh_cert - fixed certificate generation to restore original certificate + if an error is encountered (https://github.com/ansible-collections/community.crypto/pull/255). + - openssh_keypair - fixed a bug that prevented custom file attributes being + applied to public keys (https://github.com/ansible-collections/community.crypto/pull/257). + minor_changes: + - Avoid internal ansible-core module_utils in favor of equivalent public API + available since at least Ansible 2.9 (https://github.com/ansible-collections/community.crypto/pull/253). + - openssh certificate module utils - new module_utils for parsing OpenSSH certificates + (https://github.com/ansible-collections/community.crypto/pull/246). + - openssh_cert - added ``regenerate`` option to validate additional certificate + parameters which trigger regeneration of an existing certificate (https://github.com/ansible-collections/community.crypto/pull/256). + - openssh_cert - adding ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/255). + release_summary: Regular bugfix and feature release. + fragments: + - 1.8.0.yml + - 246-openssh-certificate-module-utils.yml + - 255-openssh_cert-adding-diff-support.yml + - 256-openssh_cert-adding-idempotency-option.yml + - 257-openssh-keypair-fix-pubkey-permissions.yml + - ansible-core-_text.yml + release_date: '2021-08-10' + 1.9.0: + changes: + bugfixes: + - keypair_backend module utils - simplify code to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263). + - openssh_keypair - fixed ``cryptography`` backend to preserve original file + permissions when regenerating a keypair requires existing files to be overwritten + (https://github.com/ansible-collections/community.crypto/pull/260). + - openssh_keypair - fixed error handling to restore original keypair if regeneration + fails (https://github.com/ansible-collections/community.crypto/pull/260). + - x509_crl - restore inherited function signature to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263). + minor_changes: + - get_certificate - added ``starttls`` option to retrieve certificates from + servers which require clients to request an encrypted connection (https://github.com/ansible-collections/community.crypto/pull/264). + - openssh_keypair - added ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/260). + release_summary: Regular feature release. + fragments: + - 1.9.0.yml + - 260-openssh_keypair-diff-support.yml + - 263-sanity.yml + - 264-get_certificate-add-starttls-option.yml + release_date: '2021-08-30' + 1.9.1: + changes: + release_summary: Accidental 1.9.1 release. Identical to 1.9.0. + release_date: '2021-08-30' + 1.9.2: + changes: + release_summary: Bugfix release to fix the changelog. No other change compared + to 1.9.0. + fragments: + - 1.9.2.yml + release_date: '2021-08-30' + 1.9.3: + changes: + bugfixes: + - openssl_csr and openssl_csr_pipe - make sure that Unicode strings are used + to compare strings with the cryptography backend. This fixes idempotency problems + with non-ASCII letters on Python 2 (https://github.com/ansible-collections/community.crypto/issues/270, + https://github.com/ansible-collections/community.crypto/pull/271). + release_summary: Regular bugfix release. + fragments: + - 1.9.3.yml + - 271-openssl_csr-utf8.yml + release_date: '2021-09-14' + 1.9.4: + changes: + bugfixes: + - acme_* modules - fix commands composed for OpenSSL backend to retrieve information + on CSRs and certificates from stdin to use ``/dev/stdin`` instead of ``-``. + This is needed for OpenSSL 1.0.1 and 1.0.2, apparently (https://github.com/ansible-collections/community.crypto/pull/279). + - acme_challenge_cert_helper - only return exception when cryptography is not + installed, not when a too old version of it is installed. This prevents Ansible's + callback to crash (https://github.com/ansible-collections/community.crypto/pull/281). + release_summary: Regular bugfix release. + fragments: + - 1.9.4.yml + - 279-acme-openssl.yml + - 282-acme_challenge_cert_helper-error.yml + release_date: '2021-09-28' + 2.0.0: + changes: + breaking_changes: + - Adjust ``dirName`` text parsing and to text converting code to conform to + `Sections 2 and 3 of RFC 4514 <https://datatracker.ietf.org/doc/html/rfc4514.html>`_. + This is similar to how `cryptography handles this <https://cryptography.io/en/latest/x509/reference/#cryptography.x509.Name.rfc4514_string>`_ + (https://github.com/ansible-collections/community.crypto/pull/274). + - acme module utils - removing compatibility code (https://github.com/ansible-collections/community.crypto/pull/290). + - acme_* modules - removed vendored copy of the Python library ``ipaddress``. + If you are using Python 2.x, please make sure to install the library (https://github.com/ansible-collections/community.crypto/pull/287). + - compatibility module_utils - removed vendored copy of the Python library ``ipaddress`` + (https://github.com/ansible-collections/community.crypto/pull/287). + - crypto module utils - removing compatibility code (https://github.com/ansible-collections/community.crypto/pull/290). + - get_certificate, openssl_csr_info, x509_certificate_info - depending on the + ``cryptography`` version used, the modules might not return the ASN.1 value + for an extension as contained in the certificate respectively CSR, but a re-encoded + version of it. This should usually be identical to the value contained in + the source file, unless the value was malformed. For extensions not handled + by C(cryptography) the value contained in the source file is always returned + unaltered (https://github.com/ansible-collections/community.crypto/pull/318). + - module_utils - removed various PyOpenSSL support functions and default backend + values that are not needed for the openssl_pkcs12 module (https://github.com/ansible-collections/community.crypto/pull/273). + - openssl_csr, openssl_csr_pipe, x509_crl - the ``subject`` respectively ``issuer`` + fields no longer ignore empty values, but instead fail when encountering them + (https://github.com/ansible-collections/community.crypto/pull/316). + - openssl_privatekey_info - by default consistency checks are not run; they + need to be explicitly requested by passing ``check_consistency=true`` (https://github.com/ansible-collections/community.crypto/pull/309). + - x509_crl - for idempotency checks, the ``issuer`` order is ignored. If order + is important, use the new ``issuer_ordered`` option (https://github.com/ansible-collections/community.crypto/pull/316). + bugfixes: + - cryptography backend - improve Unicode handling for Python 2 (https://github.com/ansible-collections/community.crypto/pull/313). + - get_certificate - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294). + - openssl_csr_info - fix compatibility with the cryptography 35.0.0 release + (https://github.com/ansible-collections/community.crypto/pull/294). + - openssl_pkcs12 - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/296). + - x509_certificate_info - fix compatibility with the cryptography 35.0.0 release + (https://github.com/ansible-collections/community.crypto/pull/294). + deprecated_features: + - acme_* modules - ACME version 1 is now deprecated and support for it will + be removed in community.crypto 2.0.0 (https://github.com/ansible-collections/community.crypto/pull/288). + minor_changes: + - acme_certificate - the ``subject`` and ``issuer`` fields in in the ``select_chain`` + entries are now more strictly validated (https://github.com/ansible-collections/community.crypto/pull/316). + - openssl_csr, openssl_csr_pipe - provide a new ``subject_ordered`` option if + the order of the components in the subject is of importance (https://github.com/ansible-collections/community.crypto/issues/291, + https://github.com/ansible-collections/community.crypto/pull/316). + - openssl_csr, openssl_csr_pipe - there is now stricter validation of the values + of the ``subject`` option (https://github.com/ansible-collections/community.crypto/pull/316). + - openssl_privatekey_info - add ``check_consistency`` option to request private + key consistency checks to be done (https://github.com/ansible-collections/community.crypto/pull/309). + - x509_certificate, x509_certificate_pipe - add ``ignore_timestamps`` option + which allows to enable idempotency for 'not before' and 'not after' options + (https://github.com/ansible-collections/community.crypto/issues/295, https://github.com/ansible-collections/community.crypto/pull/317). + - x509_crl - provide a new ``issuer_ordered`` option if the order of the components + in the issuer is of importance (https://github.com/ansible-collections/community.crypto/issues/291, + https://github.com/ansible-collections/community.crypto/pull/316). + - x509_crl - there is now stricter validation of the values of the ``issuer`` + option (https://github.com/ansible-collections/community.crypto/pull/316). + release_summary: 'A new major release of the ``community.crypto`` collection. + The main changes are removal of the PyOpenSSL backends for almost all modules + (``openssl_pkcs12`` being the only exception), and removal of the ``assertonly`` + provider in the ``x509_certificate`` provider. There are also some other breaking + changes which should improve the user interface/experience of this collection + long-term. + + ' + removed_features: + - acme_* modules - the ``acme_directory`` option is now required (https://github.com/ansible-collections/community.crypto/pull/290). + - acme_* modules - the ``acme_version`` option is now required (https://github.com/ansible-collections/community.crypto/pull/290). + - acme_account_facts - the deprecated redirect has been removed. Use community.crypto.acme_account_info + instead (https://github.com/ansible-collections/community.crypto/pull/290). + - acme_account_info - ``retrieve_orders=url_list`` no longer returns the return + value ``orders``. Use the ``order_uris`` return value instead (https://github.com/ansible-collections/community.crypto/pull/290). + - crypto.info module utils - the deprecated redirect has been removed. Use ``crypto.pem`` + instead (https://github.com/ansible-collections/community.crypto/pull/290). + - get_certificate - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - openssl_certificate - the deprecated redirect has been removed. Use community.crypto.x509_certificate + instead (https://github.com/ansible-collections/community.crypto/pull/290). + - openssl_certificate_info - the deprecated redirect has been removed. Use community.crypto.x509_certificate_info + instead (https://github.com/ansible-collections/community.crypto/pull/290). + - openssl_csr - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - openssl_csr and openssl_csr_pipe - ``version`` now only accepts the (default) + value 1 (https://github.com/ansible-collections/community.crypto/pull/290). + - openssl_csr_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - openssl_csr_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - openssl_privatekey - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - openssl_privatekey_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - openssl_privatekey_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - openssl_publickey - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - openssl_publickey_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - openssl_signature - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - openssl_signature_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - x509_certificate - remove ``assertonly`` provider (https://github.com/ansible-collections/community.crypto/pull/289). + - x509_certificate - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - x509_certificate_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + - x509_certificate_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273). + fragments: + - 2.0.0.yml + - 273-pyopenssl-removal.yml + - 274-dirname-rfc4514.yml + - 287-remove-ipaddress.yml + - 288-depecate-acme-v1.yml + - 289-assertonly-removed.yml + - 290-remove-deprecations.yml + - 294-cryptography-35.0.0.yml + - 296-openssl_pkcs12-cryptography-35.yml + - 309-openssl_privatekey_info-consistency.yml + - 313-unicode-names.yml + - 315-ordered-names.yml + - 317-ignore-timestamps.yml + - 318-extension-value-note.yml + release_date: '2021-11-01' + 2.0.1: + changes: + bugfixes: + - acme_certificate - avoid passing multiple certificates to ``cryptography``'s + X.509 certificate loader when ``fullchain_dest`` is used (https://github.com/ansible-collections/community.crypto/pull/324). + - get_certificate, openssl_csr_info, x509_certificate_info - add fallback code + for extension parsing that works with cryptography 36.0.0 and newer. This + code re-serializes de-serialized extensions and thus can return slightly different + values if the extension in the original CSR resp. certificate was not canonicalized + correctly. This code is currently used as a fallback if the existing code + stops working, but we will switch it to be the main code in a future release + (https://github.com/ansible-collections/community.crypto/pull/331). + - luks_device - now also runs a built-in LUKS signature cleaner on ``state=absent`` + to make sure that also the secondary LUKS2 header is wiped when older versions + of wipefs are used (https://github.com/ansible-collections/community.crypto/issues/326, + https://github.com/ansible-collections/community.crypto/pull/327). + - openssl_pkcs12 - use new PKCS#12 deserialization infrastructure from cryptography + 36.0.0 if available (https://github.com/ansible-collections/community.crypto/pull/302). + minor_changes: + - acme_* modules - fix usage of ``fetch_url`` with changes in latest ansible-core + ``devel`` branch (https://github.com/ansible-collections/community.crypto/pull/339). + release_summary: Bugfix release with extra forward compatibility for newer versions + of cryptography. + fragments: + - 2.0.1.yml + - 302-openssl_pkcs12-cryptography-36.0.0.yml + - 324-acme_certificate-fullchain.yml + - 327-luks_device-wipe.yml + - 331-cryptography-extensions.yml + - fetch_url-devel.yml + release_date: '2021-11-22' + 2.0.2: + changes: + release_summary: Documentation fix release. No actual code changes. + fragments: + - 2.0.2.yml + release_date: '2021-12-20' + 2.1.0: + changes: + bugfixes: + - Various modules and plugins - use vendored version of ``distutils.version`` + instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.crypto/pull/353). + - certificate_complete_chain - do not append root twice if the chain already + ends with a root certificate (https://github.com/ansible-collections/community.crypto/pull/360). + - certificate_complete_chain - do not hang when infinite loop is found (https://github.com/ansible-collections/community.crypto/issues/355, + https://github.com/ansible-collections/community.crypto/pull/360). + minor_changes: + - Adjust error messages that indicate ``cryptography`` is not installed from + ``Can't`` to ``Cannot`` (https://github.com/ansible-collections/community.crypto/pull/374). + release_summary: Feature and bugfix release. + fragments: + - 2.1.0.yml + - 353-distutils.version.yml + - 360-certificate_complete_chain-loop.yml + - 374-docs.yml + modules: + - description: Retrieve cryptographic capabilities + name: crypto_info + namespace: '' + - description: Convert OpenSSL private keys + name: openssl_privatekey_convert + namespace: '' + release_date: '2022-01-10' + 2.10.0: + changes: + bugfixes: + - openssl_csr, openssl_csr_pipe - prevent invalid values for ``crl_distribution_points`` + that do not have one of ``full_name``, ``relative_name``, and ``crl_issuer`` + (https://github.com/ansible-collections/community.crypto/pull/560). + - openssl_publickey_info - do not crash with internal error when public key + cannot be parsed (https://github.com/ansible-collections/community.crypto/pull/551). + release_summary: Bugfix and feature release. + fragments: + - 2.10.0.yml + - 551-publickey-info.yml + - 560-openssl_csr-crl_distribution_points.yml + plugins: + filter: + - description: Retrieve information from OpenSSL Certificate Signing Requests + (CSR) + name: openssl_csr_info + namespace: null + - description: Retrieve information from OpenSSL private keys + name: openssl_privatekey_info + namespace: null + - description: Retrieve information from OpenSSL public keys in PEM format + name: openssl_publickey_info + namespace: null + - description: Split PEM file contents into multiple objects + name: split_pem + namespace: null + - description: Retrieve information from X.509 certificates in PEM format + name: x509_certificate_info + namespace: null + - description: Retrieve information from X.509 CRLs in PEM format + name: x509_crl_info + namespace: null + release_date: '2023-01-02' + 2.11.0: + changes: + bugfixes: + - action plugin helper - fix handling of deprecations for ansible-core 2.14.2 + (https://github.com/ansible-collections/community.crypto/pull/572). + - execution environment binary dependencies (bindep.txt) - fix ``python3-pyOpenSSL`` + dependency resolution on RHEL 9+ / CentOS Stream 9+ platforms (https://github.com/ansible-collections/community.crypto/pull/575). + - various plugins - remove unnecessary imports (https://github.com/ansible-collections/community.crypto/pull/569). + minor_changes: + - get_certificate - adds ``ciphers`` option for custom cipher selection (https://github.com/ansible-collections/community.crypto/pull/571). + release_summary: Feature and bugfix release. + fragments: + - 2.11.0.yml + - 571_get_certificate_ciphers.yaml + - 572-action-module.yml + - 575-bindep-python3-pyOpenSSL.yml + - remove-unneeded-imports.yml + release_date: '2023-02-23' + 2.11.1: + changes: + release_summary: Maintenance release with improved documentation. + fragments: + - 2.11.1.yml + release_date: '2023-03-24' + 2.12.0: + changes: + minor_changes: + - get_certificate - add ``asn1_base64`` option to control whether the ASN.1 + included in the ``extensions`` return value is binary data or Base64 encoded + (https://github.com/ansible-collections/community.crypto/pull/592). + release_summary: Feature release. + fragments: + - 2.12.0.yml + - 592-get_certificate-base64.yml + release_date: '2023-04-16' + 2.13.0: + changes: + bugfixes: + - openssh_keypair - always generate a new key pair if the private key does not + exist. Previously, the module would fail when ``regenerate=fail`` without + an existing key, contradicting the documentation (https://github.com/ansible-collections/community.crypto/pull/598). + - x509_crl - remove problem with ansible-core 2.16 due to ``AnsibleModule`` + is now validating the ``mode`` parameter's values (https://github.com/ansible-collections/community.crypto/issues/596). + deprecated_features: + - x509_crl - the ``mode`` option is deprecated; use ``crl_mode`` instead. The + ``mode`` option will change its meaning in community.crypto 3.0.0, and will + refer to the CRL file's mode instead (https://github.com/ansible-collections/community.crypto/issues/596). + minor_changes: + - x509_crl - the ``crl_mode`` option has been added to replace the existing + ``mode`` option (https://github.com/ansible-collections/community.crypto/issues/596). + release_summary: Bugfix and maintenance release. + fragments: + - 2.13.0.yml + - 596-x509_crl-mode.yml + - 598-openssh_keypair-generate-new-key.yml + release_date: '2023-05-01' + 2.13.1: + changes: + bugfixes: + - execution environment definition - fix installation of ``python3-pyOpenSSL`` + package on CentOS and RHEL (https://github.com/ansible-collections/community.crypto/pull/606). + - execution environment definition - fix source of ``python3-pyOpenSSL`` package + for Rocky Linux 9+ (https://github.com/ansible-collections/community.crypto/pull/606). + release_summary: Bugfix release. + fragments: + - 2.13.1.yml + - 606-ee-rocky.yml + release_date: '2023-05-21' + 2.14.0: + changes: + minor_changes: + - acme_certificate - allow to use no challenge by providing ``no challenge`` + for the ``challenge`` option. This is needed for ACME servers where validation + is done without challenges (https://github.com/ansible-collections/community.crypto/issues/613, + https://github.com/ansible-collections/community.crypto/pull/615). + - acme_certificate - validate and wait for challenges in parallel instead handling + them one after another (https://github.com/ansible-collections/community.crypto/pull/617). + - x509_certificate_info - added support for certificates in DER format when + using ``path`` parameter (https://github.com/ansible-collections/community.crypto/issues/603). + release_summary: Feature release. + fragments: + - 2.14.0.yml + - 615-no-challenge.yml + - 617-acme_certificate-parallel.yml + - 622-der-format-support.yml + release_date: '2023-06-15' + 2.2.0: + changes: + bugfixes: + - luks_devices - set ``LANG`` and similar environment variables to avoid translated + output, which can break some of the module's functionality like key management + (https://github.com/ansible-collections/community.crypto/pull/388, https://github.com/ansible-collections/community.crypto/issues/385). + minor_changes: + - openssh_cert - added ``ignore_timestamps`` parameter so it can be used semi-idempotent + with relative timestamps in ``valid_to``/``valid_from`` (https://github.com/ansible-collections/community.crypto/issues/379). + release_summary: Regular bugfix and feature release. + fragments: + - 2.2.0.yml + - 381_openssh_cert_add_ignore_timestamps.yml + - 388-luks_device-i18n.yml + release_date: '2022-02-01' + 2.2.1: + changes: + bugfixes: + - openssh_cert - fixed false ``changed`` status for ``host`` certificates when + using ``full_idempotence`` (https://github.com/ansible-collections/community.crypto/issues/395, + https://github.com/ansible-collections/community.crypto/pull/396). + release_summary: Bugfix release. + fragments: + - 2.2.1.yml + - 396-openssh_cert-host-cert-idempotence-fix.yml + release_date: '2022-02-05' + 2.2.2: + changes: + bugfixes: + - certificate_complete_chain - allow multiple potential intermediate certificates + to have the same subject (https://github.com/ansible-collections/community.crypto/issues/399, + https://github.com/ansible-collections/community.crypto/pull/403). + - x509_certificate - for the ``ownca`` provider, check whether the CA private + key actually belongs to the CA certificate (https://github.com/ansible-collections/community.crypto/pull/407). + - x509_certificate - regenerate certificate when the CA's public key changes + for ``provider=ownca`` (https://github.com/ansible-collections/community.crypto/pull/407). + - x509_certificate - regenerate certificate when the CA's subject changes for + ``provider=ownca`` (https://github.com/ansible-collections/community.crypto/issues/400, + https://github.com/ansible-collections/community.crypto/pull/402). + - x509_certificate - regenerate certificate when the private key changes for + ``provider=selfsigned`` (https://github.com/ansible-collections/community.crypto/pull/407). + release_summary: 'Regular bugfix release. + + + In this release, we extended the test matrix to include Alpine 3, ArchLinux, + Debian Bullseye, and CentOS Stream 8. CentOS 8 was removed from the test matrix. + + ' + fragments: + - 2.2.2.yml + - 402-x509_certificate-ownca-subject.yml + - 403-certificate_complete_chain-same-subject.yml + - 407-x509_certificate-signature.yml + release_date: '2022-02-21' + 2.2.3: + changes: + bugfixes: + - luks_device - fix parsing of ``lsblk`` output when device name ends with ``crypt`` + (https://github.com/ansible-collections/community.crypto/issues/409, https://github.com/ansible-collections/community.crypto/pull/410). + release_summary: Regular bugfix release. + fragments: + - 2.2.3.yml + - 410-luks_device-lsblk-parsing.yml + release_date: '2022-03-04' + 2.2.4: + changes: + bugfixes: + - openssh_* modules - fix exception handling to report traceback to users for + enhanced traceability (https://github.com/ansible-collections/community.crypto/pull/417). + release_summary: Regular maintenance release. + fragments: + - 2.2.4.yml + - 417-openssh_modules-fix-exception-reporting.yml + release_date: '2022-03-22' + 2.3.0: + changes: + bugfixes: + - Make collection more robust when PyOpenSSL is used with an incompatible cryptography + version (https://github.com/ansible-collections/community.crypto/pull/445). + - x509_crl - fix crash when ``issuer`` for a revoked certificate is specified + (https://github.com/ansible-collections/community.crypto/pull/441). + minor_changes: + - Prepare collection for inclusion in an Execution Environment by declaring + its dependencies. Please note that system packages are used for cryptography + and PyOpenSSL, which can be rather limited. If you need features from newer + cryptography versions, you will have to manually force a newer version to + be installed by pip by specifying something like ``cryptography >= 37.0.0`` + in your Execution Environment's Python dependencies file (https://github.com/ansible-collections/community.crypto/pull/440). + - Support automatic conversion for Internalionalized Domain Names (IDNs). When + passing general names, for example Subject Altenative Names to ``community.crypto.openssl_csr``, + these will automatically be converted to IDNA. Conversion will be done per + label to IDNA2008 if possible, and IDNA2003 if IDNA2008 conversion fails for + that label. Note that IDNA conversion requires `the Python idna library <https://pypi.org/project/idna/>`_ + to be installed. Please note that depending on which versions of the cryptography + library are used, it could try to process the converted IDNA another time + with the Python ``idna`` library and reject IDNA2003 encoded values. Using + a new enough ``cryptography`` version avoids this (https://github.com/ansible-collections/community.crypto/issues/426, + https://github.com/ansible-collections/community.crypto/pull/436). + - acme_* modules - add parameter ``request_timeout`` to manage HTTP(S) request + timeout (https://github.com/ansible-collections/community.crypto/issues/447, + https://github.com/ansible-collections/community.crypto/pull/448). + - luks_devices - added ``perf_same_cpu_crypt``, ``perf_submit_from_crypt_cpus``, + ``perf_no_read_workqueue``, ``perf_no_write_workqueue`` for performance tuning + when opening LUKS2 containers (https://github.com/ansible-collections/community.crypto/issues/427). + - luks_devices - added ``persistent`` option when opening LUKS2 containers (https://github.com/ansible-collections/community.crypto/pull/434). + - openssl_csr_info - add ``name_encoding`` option to control the encoding (IDNA, + Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436). + - openssl_pkcs12 - allow to provide the private key as text instead of having + to read it from a file. This allows to store the private key in an encrypted + form, for example in Ansible Vault (https://github.com/ansible-collections/community.crypto/pull/452). + - x509_certificate_info - add ``name_encoding`` option to control the encoding + (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436). + - x509_crl - add ``name_encoding`` option to control the encoding (IDNA, Unicode) + used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436). + - x509_crl_info - add ``name_encoding`` option to control the encoding (IDNA, + Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436). + release_summary: Feature and bugfix release. + fragments: + - 2.3.0.yml + - 434-add-persistent-and-perf-options.yml + - 436-idns.yml + - 440-ee.yml + - 441-x509-crl-cert-issuer.yml + - 445-fix.yml + - 448-acme-request-timeouts.yml + - 452-openssl_pkcs12-private-key-content.yml + release_date: '2022-05-09' + 2.3.1: + changes: + bugfixes: + - Include ``PSF-license.txt`` file for ``plugins/module_utils/_version.py``. + release_summary: Maintenance release. + fragments: + - 2.3.1.yml + - psf-license.yml + release_date: '2022-05-16' + 2.3.2: + changes: + bugfixes: + - Include ``simplified_bsd.txt`` license file for the ECS module utils. + - certificate_complete_chain - do not stop execution if an unsupported signature + algorithm is encountered; warn instead (https://github.com/ansible-collections/community.crypto/pull/457). + release_summary: Maintenance and bugfix release. + fragments: + - 2.3.2.yml + - 457-certificate_complete_chain-unsupported-algorithm.yml + - simplified-bsd-license.yml + release_date: '2022-06-02' + 2.3.3: + changes: + bugfixes: + - Include ``Apache-2.0.txt`` file for ``plugins/module_utils/crypto/_obj2txt.py`` + and ``plugins/module_utils/crypto/_objects_data.py``. + - openssl_csr - the module no longer crashes with 'permitted_subtrees/excluded_subtrees + must be a non-empty list or None' if only one of ``name_constraints_permitted`` + and ``name_constraints_excluded`` is provided (https://github.com/ansible-collections/community.crypto/issues/481). + - x509_crl - do not crash when signing CRL with Ed25519 or Ed448 keys (https://github.com/ansible-collections/community.crypto/issues/473, + https://github.com/ansible-collections/community.crypto/pull/474). + release_summary: Bugfix release. + fragments: + - 2.3.3.yml + - 474-x509_crl-ed25519-ed448.yml + - 481-fix-excluded_subtrees-must-be-a-non-empty-list-or-None.yml + - apache-license.yml + release_date: '2022-06-17' + 2.3.4: + changes: + release_summary: 'Re-release of what was intended to be 2.3.3. + + + A mistake during the release process caused the 2.3.3 tag to end up on the + + commit for 1.9.17, which caused the release pipeline to re-publish 1.9.17 + + as 2.3.3. + + + This release is identical to what should have been 2.3.3, except that the + + version number has been bumped to 2.3.4 and this changelog entry for 2.3.4 + + has been added. + + ' + fragments: + - 2.3.4.yml + release_date: '2022-06-21' + 2.4.0: + changes: + bugfixes: + - openssl_pkcs12 - when using the pyOpenSSL backend, do not crash when trying + to read non-existing other certificates (https://github.com/ansible-collections/community.crypto/issues/486, + https://github.com/ansible-collections/community.crypto/pull/487). + deprecated_features: + - Support for Ansible 2.9 and ansible-base 2.10 is deprecated, and will be removed + in the next major release (community.crypto 3.0.0). Some modules might still + work with these versions afterwards, but we will no longer keep compatibility + code that was needed to support them (https://github.com/ansible-collections/community.crypto/pull/460). + release_summary: Deprecation and bugfix release. No new features this time. + fragments: + - 2.4.0.yml + - 487-openssl_pkcs12-other-certs-crash.yml + - deprecate-ansible-2.9-2.10.yml + release_date: '2022-07-09' + 2.5.0: + changes: + minor_changes: + - All software licenses are now in the ``LICENSES/`` directory of the collection + root. Moreover, ``SPDX-License-Identifier:`` is used to declare the applicable + license for every file that is not automatically generated (https://github.com/ansible-collections/community.crypto/pull/491). + release_summary: Maintenance release with improved licensing declaration and + documentation fixes. + fragments: + - 2.5.0.yml + - 491-licenses.yml + release_date: '2022-08-04' + 2.6.0: + changes: + minor_changes: + - acme* modules - support the HTTP 429 Too Many Requests response status (https://github.com/ansible-collections/community.crypto/pull/508). + - openssh_keypair - added ``pkcs1``, ``pkcs8``, and ``ssh`` to the available + choices for the ``private_key_format`` option (https://github.com/ansible-collections/community.crypto/pull/511). + release_summary: Feature release. + fragments: + - 2.6.0.yml + - 508-acme-429.yml + - 511-openssh_keypair-private_key_format_options.yml + release_date: '2022-09-19' + 2.7.0: + changes: + bugfixes: + - openssl_privatekey_pipe - ensure compatibility with newer versions of ansible-core + (https://github.com/ansible-collections/community.crypto/pull/515). + minor_changes: + - acme* modules - also support the HTTP 503 Service Unavailable and 408 Request + Timeout response status for automatic retries (https://github.com/ansible-collections/community.crypto/pull/513). + release_summary: Feature release. + fragments: + - 2.7.0.yml + - 513-acme-503.yml + - 515-action-module-compat.yml + release_date: '2022-09-23' + 2.7.1: + changes: + bugfixes: + - acme_* modules - improve feedback when importing ``cryptography`` does not + work (https://github.com/ansible-collections/community.crypto/issues/518, + https://github.com/ansible-collections/community.crypto/pull/519). + release_summary: Maintenance release. + fragments: + - 2.7.1.yml + - 519-acme-cryptography.yml + release_date: '2022-10-17' + 2.8.0: + changes: + minor_changes: + - acme_* modules - handle more gracefully if CA's new nonce call does not return + a nonce (https://github.com/ansible-collections/community.crypto/pull/525). + - acme_* modules - include symbolic HTTP status codes in error and log messages + when available (https://github.com/ansible-collections/community.crypto/pull/524). + - openssl_pkcs12 - add option ``encryption_level`` which allows to chose ``compatibility2022`` + when cryptography >= 38.0.0 is used to enable a more backwards compatible + encryption algorithm. If cryptography uses OpenSSL 3.0.0 or newer, the default + algorithm is not compatible with older software (https://github.com/ansible-collections/community.crypto/pull/523). + release_summary: Feature release. + fragments: + - 2.8.0.yml + - 523-pkcs12-compat.yml + - 524-acme-http-errors.yml + - 525-acme-no-nonce.yml + release_date: '2022-11-02' + 2.8.1: + changes: + release_summary: Maintenance release with improved documentation. + fragments: + - 2.8.1.yml + release_date: '2022-11-06' + 2.9.0: + changes: + minor_changes: + - x509_certificate_info - adds ``issuer_uri`` field in return value based on + Authority Information Access data (https://github.com/ansible-collections/community.crypto/pull/530). + release_summary: Regular feature release. + fragments: + - 2.9.0.yml + - aia_issuer.yaml + release_date: '2022-11-27' diff --git a/ansible_collections/community/crypto/changelogs/changelog.yaml.license b/ansible_collections/community/crypto/changelogs/changelog.yaml.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/changelogs/changelog.yaml.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/changelogs/config.yaml b/ansible_collections/community/crypto/changelogs/config.yaml new file mode 100644 index 000000000..f2767048a --- /dev/null +++ b/ansible_collections/community/crypto/changelogs/config.yaml @@ -0,0 +1,33 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +changelog_filename_template: ../CHANGELOG.rst +changelog_filename_version_depth: 0 +changes_file: changelog.yaml +changes_format: combined +keep_fragments: false +mention_ancestor: true +new_plugins_after_name: removed_features +notesdir: fragments +prelude_section_name: release_summary +prelude_section_title: Release Summary +sections: +- - major_changes + - Major Changes +- - minor_changes + - Minor Changes +- - breaking_changes + - Breaking Changes / Porting Guide +- - deprecated_features + - Deprecated Features +- - removed_features + - Removed Features (previously deprecated) +- - security_fixes + - Security Fixes +- - bugfixes + - Bugfixes +- - known_issues + - Known Issues +title: Community Crypto diff --git a/ansible_collections/community/crypto/changelogs/fragments/.keep b/ansible_collections/community/crypto/changelogs/fragments/.keep new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/crypto/changelogs/fragments/.keep diff --git a/ansible_collections/community/crypto/docs/docsite/extra-docs.yml b/ansible_collections/community/crypto/docs/docsite/extra-docs.yml new file mode 100644 index 000000000..a7f9c146a --- /dev/null +++ b/ansible_collections/community/crypto/docs/docsite/extra-docs.yml @@ -0,0 +1,10 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +sections: + - title: Scenario Guides + toctree: + - guide_selfsigned + - guide_ownca diff --git a/ansible_collections/community/crypto/docs/docsite/links.yml b/ansible_collections/community/crypto/docs/docsite/links.yml new file mode 100644 index 000000000..1316340ec --- /dev/null +++ b/ansible_collections/community/crypto/docs/docsite/links.yml @@ -0,0 +1,27 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +edit_on_github: + repository: ansible-collections/community.crypto + branch: main + path_prefix: '' + +extra_links: + - description: Submit a bug report + url: https://github.com/ansible-collections/community.crypto/issues/new?assignees=&labels=&template=bug_report.md + - description: Request a feature + url: https://github.com/ansible-collections/community.crypto/issues/new?assignees=&labels=&template=feature_request.md + +communication: + matrix_rooms: + - topic: General usage and support questions + room: '#users:ansible.im' + irc_channels: + - topic: General usage and support questions + network: Libera + channel: '#ansible' + mailing_lists: + - topic: Ansible Project List + url: https://groups.google.com/g/ansible-project diff --git a/ansible_collections/community/crypto/docs/docsite/rst/guide_ownca.rst b/ansible_collections/community/crypto/docs/docsite/rst/guide_ownca.rst new file mode 100644 index 000000000..0e8a46da8 --- /dev/null +++ b/ansible_collections/community/crypto/docs/docsite/rst/guide_ownca.rst @@ -0,0 +1,153 @@ +.. + Copyright (c) Ansible Project + GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) + SPDX-License-Identifier: GPL-3.0-or-later + +.. _ansible_collections.community.crypto.docsite.guide_ownca: + +How to create a small CA +======================== + +The `community.crypto collection <https://galaxy.ansible.com/community/crypto>`_ offers multiple modules that create private keys, certificate signing requests, and certificates. This guide shows how to create your own small CA and how to use it to sign certificates. + +In all examples, we assume that the CA's private key is password protected, where the password is provided in the ``secret_ca_passphrase`` variable. + +Set up the CA +------------- + +Any certificate can be used as a CA certificate. You can create a self-signed certificate (see :ref:`ansible_collections.community.crypto.docsite.guide_selfsigned`), use another CA certificate to sign a new certificate (using the instructions below for signing a certificate), ask (and pay) a commercial CA to sign your CA certificate, etc. + +The following instructions show how to set up a simple self-signed CA certificate. + +.. code-block:: yaml+jinja + + - name: Create private key with password protection + community.crypto.openssl_privatekey: + path: /path/to/ca-certificate.key + passphrase: "{{ secret_ca_passphrase }}" + + - name: Create certificate signing request (CSR) for CA certificate + community.crypto.openssl_csr_pipe: + privatekey_path: /path/to/ca-certificate.key + privatekey_passphrase: "{{ secret_ca_passphrase }}" + common_name: Ansible CA + use_common_name_for_san: false # since we do not specify SANs, don't use CN as a SAN + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: true + key_usage: + - keyCertSign + key_usage_critical: true + register: ca_csr + + - name: Create self-signed CA certificate from CSR + community.crypto.x509_certificate: + path: /path/to/ca-certificate.pem + csr_content: "{{ ca_csr.csr }}" + privatekey_path: /path/to/ca-certificate.key + privatekey_passphrase: "{{ secret_ca_passphrase }}" + provider: selfsigned + +Use the CA to sign a certificate +-------------------------------- + +To sign a certificate, you must pass a CSR to the :ref:`community.crypto.x509_certificate module <ansible_collections.community.crypto.x509_certificate_module>` or :ref:`community.crypto.x509_certificate_pipe module <ansible_collections.community.crypto.x509_certificate_pipe_module>`. + +In the following example, we assume that the certificate to sign (including its private key) are on ``server_1``, while our CA certificate is on ``server_2``. We do not want any key material to leave each respective server. + +.. code-block:: yaml+jinja + + - name: Create private key for new certificate on server_1 + community.crypto.openssl_privatekey: + path: /path/to/certificate.key + delegate_to: server_1 + run_once: true + + - name: Create certificate signing request (CSR) for new certificate + community.crypto.openssl_csr_pipe: + privatekey_path: /path/to/certificate.key + subject_alt_name: + - "DNS:ansible.com" + - "DNS:www.ansible.com" + - "DNS:docs.ansible.com" + delegate_to: server_1 + run_once: true + register: csr + + - name: Sign certificate with our CA + community.crypto.x509_certificate_pipe: + csr_content: "{{ csr.csr }}" + provider: ownca + ownca_path: /path/to/ca-certificate.pem + ownca_privatekey_path: /path/to/ca-certificate.key + ownca_privatekey_passphrase: "{{ secret_ca_passphrase }}" + ownca_not_after: +365d # valid for one year + ownca_not_before: "-1d" # valid since yesterday + delegate_to: server_2 + run_once: true + register: certificate + + - name: Write certificate file on server_1 + copy: + dest: /path/to/certificate.pem + content: "{{ certificate.certificate }}" + delegate_to: server_1 + run_once: true + +Please note that the above procedure is **not idempotent**. The following extended example reads the existing certificate from ``server_1`` (if exists) and provides it to the :ref:`community.crypto.x509_certificate_pipe module <ansible_collections.community.crypto.x509_certificate_pipe_module>`, and only writes the result back if it was changed: + +.. code-block:: yaml+jinja + + - name: Create private key for new certificate on server_1 + community.crypto.openssl_privatekey: + path: /path/to/certificate.key + delegate_to: server_1 + run_once: true + + - name: Create certificate signing request (CSR) for new certificate + community.crypto.openssl_csr_pipe: + privatekey_path: /path/to/certificate.key + subject_alt_name: + - "DNS:ansible.com" + - "DNS:www.ansible.com" + - "DNS:docs.ansible.com" + delegate_to: server_1 + run_once: true + register: csr + + - name: Check whether certificate exists + stat: + path: /path/to/certificate.pem + delegate_to: server_1 + run_once: true + register: certificate_exists + + - name: Read existing certificate if exists + slurp: + src: /path/to/certificate.pem + when: certificate_exists.stat.exists + delegate_to: server_1 + run_once: true + register: certificate + + - name: Sign certificate with our CA + community.crypto.x509_certificate_pipe: + content: "{{ (certificate.content | b64decode) if certificate_exists.stat.exists else omit }}" + csr_content: "{{ csr.csr }}" + provider: ownca + ownca_path: /path/to/ca-certificate.pem + ownca_privatekey_path: /path/to/ca-certificate.key + ownca_privatekey_passphrase: "{{ secret_ca_passphrase }}" + ownca_not_after: +365d # valid for one year + ownca_not_before: "-1d" # valid since yesterday + delegate_to: server_2 + run_once: true + register: certificate + + - name: Write certificate file on server_1 + copy: + dest: /path/to/certificate.pem + content: "{{ certificate.certificate }}" + delegate_to: server_1 + run_once: true + when: certificate is changed diff --git a/ansible_collections/community/crypto/docs/docsite/rst/guide_selfsigned.rst b/ansible_collections/community/crypto/docs/docsite/rst/guide_selfsigned.rst new file mode 100644 index 000000000..dc208d5c7 --- /dev/null +++ b/ansible_collections/community/crypto/docs/docsite/rst/guide_selfsigned.rst @@ -0,0 +1,65 @@ +.. + Copyright (c) Ansible Project + GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) + SPDX-License-Identifier: GPL-3.0-or-later + +.. _ansible_collections.community.crypto.docsite.guide_selfsigned: + +How to create self-signed certificates +====================================== + +The `community.crypto collection <https://galaxy.ansible.com/community/crypto>`_ offers multiple modules that create private keys, certificate signing requests, and certificates. This guide shows how to create self-signed certificates. + +For creating any kind of certificate, you always have to start with a private key. You can use the :ref:`community.crypto.openssl_privatekey module <ansible_collections.community.crypto.openssl_privatekey_module>` to create a private key. If you only specify ``path``, the default parameters will be used. This will result in a 4096 bit RSA private key: + +.. code-block:: yaml+jinja + + - name: Create private key (RSA, 4096 bits) + community.crypto.openssl_privatekey: + path: /path/to/certificate.key + +You can specify ``type`` to select another key type, ``size`` to select a different key size (only available for RSA and DSA keys), or ``passphrase`` if you want to store the key password-protected: + +.. code-block:: yaml+jinja + + - name: Create private key (X25519) with password protection + community.crypto.openssl_privatekey: + path: /path/to/certificate.key + type: X25519 + passphrase: changeme + +To create a very simple self-signed certificate with no specific information, you can proceed directly with the :ref:`community.crypto.x509_certificate module <ansible_collections.community.crypto.x509_certificate_module>`: + +.. code-block:: yaml+jinja + + - name: Create simple self-signed certificate + community.crypto.x509_certificate: + path: /path/to/certificate.pem + privatekey_path: /path/to/certificate.key + provider: selfsigned + +(If you used ``passphrase`` for the private key, you have to provide ``privatekey_passphrase``.) + +You can use ``selfsigned_not_after`` to define when the certificate expires (default: in roughly 10 years), and ``selfsigned_not_before`` to define from when the certificate is valid (default: now). + +To define further properties of the certificate, like the subject, Subject Alternative Names (SANs), key usages, name constraints, etc., you need to first create a Certificate Signing Request (CSR) and provide it to the :ref:`community.crypto.x509_certificate module <ansible_collections.community.crypto.x509_certificate_module>`. If you do not need the CSR file, you can use the :ref:`community.crypto.openssl_csr_pipe module <ansible_collections.community.crypto.openssl_csr_pipe_module>` as in the example below. (To store it to disk, use the :ref:`community.crypto.openssl_csr module <ansible_collections.community.crypto.openssl_csr_module>` instead.) + +.. code-block:: yaml+jinja + + - name: Create certificate signing request (CSR) for self-signed certificate + community.crypto.openssl_csr_pipe: + privatekey_path: /path/to/certificate.key + common_name: ansible.com + organization_name: Ansible, Inc. + subject_alt_name: + - "DNS:ansible.com" + - "DNS:www.ansible.com" + - "DNS:docs.ansible.com" + register: csr + + - name: Create self-signed certificate from CSR + community.crypto.x509_certificate: + path: /path/to/certificate.pem + csr_content: "{{ csr.csr }}" + privatekey_path: /path/to/certificate.key + provider: selfsigned diff --git a/ansible_collections/community/crypto/meta/ee-bindep.txt b/ansible_collections/community/crypto/meta/ee-bindep.txt new file mode 100644 index 000000000..b448d5403 --- /dev/null +++ b/ansible_collections/community/crypto/meta/ee-bindep.txt @@ -0,0 +1,21 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +cryptsetup [platform:dpkg] +cryptsetup [platform:rpm] +openssh-client [platform:dpkg] +openssh-clients [platform:rpm] +openssl [platform:dpkg] +openssl [platform:rpm] +python3-cryptography [platform:dpkg] +python3-cryptography [platform:rpm] +python3-openssl [platform:dpkg] +# On RHEL 9+, CentOS Stream 9+, and Rocky Linux 9+, python3-pyOpenSSL is part of EPEL +python3-pyOpenSSL [platform:rpm !platform:rhel !platform:centos !platform:rocky] +python3-pyOpenSSL [platform:rhel-8] +python3-pyOpenSSL [platform:rhel !platform:rhel-6 !platform:rhel-7 !platform:rhel-8 epel] +python3-pyOpenSSL [platform:centos-8] +python3-pyOpenSSL [platform:centos !platform:centos-6 !platform:centos-7 !platform:centos-8 epel] +python3-pyOpenSSL [platform:rocky-8] +python3-pyOpenSSL [platform:rocky !platform:rocky-8 epel] diff --git a/ansible_collections/community/crypto/meta/ee-requirements.txt b/ansible_collections/community/crypto/meta/ee-requirements.txt new file mode 100644 index 000000000..d56bc68dc --- /dev/null +++ b/ansible_collections/community/crypto/meta/ee-requirements.txt @@ -0,0 +1,5 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +PyYAML diff --git a/ansible_collections/community/crypto/meta/execution-environment.yml b/ansible_collections/community/crypto/meta/execution-environment.yml new file mode 100644 index 000000000..9da98891e --- /dev/null +++ b/ansible_collections/community/crypto/meta/execution-environment.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +version: 1 +dependencies: + python: meta/ee-requirements.txt + system: meta/ee-bindep.txt diff --git a/ansible_collections/community/crypto/meta/runtime.yml b/ansible_collections/community/crypto/meta/runtime.yml new file mode 100644 index 000000000..76500748c --- /dev/null +++ b/ansible_collections/community/crypto/meta/runtime.yml @@ -0,0 +1,34 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +requires_ansible: '>=2.9.10' + +action_groups: + acme: + - acme_inspect + - acme_certificate_revoke + - acme_certificate + - acme_account + - acme_account_info + +plugin_routing: + modules: + acme_account_facts: + tombstone: + removal_version: 2.0.0 + warning_text: The 'community.crypto.acme_account_facts' module has been renamed to 'community.crypto.acme_account_info'. + openssl_certificate: + tombstone: + removal_version: 2.0.0 + warning_text: The 'community.crypto.openssl_certificate' module has been renamed to 'community.crypto.x509_certificate' + openssl_certificate_info: + tombstone: + removal_version: 2.0.0 + warning_text: The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_certificate_info' + module_utils: + crypto.identify: + tombstone: + removal_version: 2.0.0 + warning_text: The 'crypto/identify.py' module_utils has been renamed 'crypto/pem.py'. Please update your imports diff --git a/ansible_collections/community/crypto/plugins/action/openssl_privatekey_pipe.py b/ansible_collections/community/crypto/plugins/action/openssl_privatekey_pipe.py new file mode 100644 index 000000000..dc1a16979 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/action/openssl_privatekey_pipe.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import base64 + +from ansible.module_utils.common.text.converters import to_native, to_bytes + +from ansible_collections.community.crypto.plugins.plugin_utils.action_module import ActionModuleBase + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey import ( + select_backend, + get_privatekey_argument_spec, +) + + +class PrivateKeyModule(object): + def __init__(self, module, module_backend): + self.module = module + self.module_backend = module_backend + self.check_mode = module.check_mode + self.changed = False + self.return_current_key = module.params['return_current_key'] + + if module.params['content'] is not None: + if module.params['content_base64']: + try: + data = base64.b64decode(module.params['content']) + except Exception as e: + module.fail_json(msg='Cannot decode Base64 encoded data: {0}'.format(e)) + else: + data = to_bytes(module.params['content']) + module_backend.set_existing(data) + + def generate(self, module): + """Generate a keypair.""" + + if self.module_backend.needs_regeneration(): + # Regenerate + if not self.check_mode: + self.module_backend.generate_private_key() + privatekey_data = self.module_backend.get_private_key_data() + self.privatekey_bytes = privatekey_data + self.changed = True + elif self.module_backend.needs_conversion(): + # Convert + if not self.check_mode: + self.module_backend.convert_private_key() + privatekey_data = self.module_backend.get_private_key_data() + self.privatekey_bytes = privatekey_data + self.changed = True + + def dump(self): + """Serialize the object into a dictionary.""" + result = self.module_backend.dump(include_key=self.changed or self.return_current_key) + result['changed'] = self.changed + return result + + +class ActionModule(ActionModuleBase): + @staticmethod + def setup_module(): + argument_spec = get_privatekey_argument_spec() + argument_spec.argument_spec.update(dict( + content=dict(type='str', no_log=True), + content_base64=dict(type='bool', default=False), + return_current_key=dict(type='bool', default=False), + )) + return argument_spec, dict( + supports_check_mode=True, + ) + + @staticmethod + def run_module(module): + backend, module_backend = select_backend( + module=module, + backend=module.params['select_crypto_backend'], + ) + + try: + private_key = PrivateKeyModule(module, module_backend) + private_key.generate(module) + result = private_key.dump() + if private_key.return_current_key: + # In case the module's input (`content`) is returned as `privatekey`: + # Since `content` is no_log=True, `privatekey`'s value will get replaced by + # VALUE_SPECIFIED_IN_NO_LOG_PARAMETER. To avoid this, we remove the value of + # `content` from module.no_log_values. Since we explicitly set + # `module.no_log = True`, this should be safe. + module.no_log = True + try: + module.no_log_values.remove(module.params['content']) + except KeyError: + pass + module.params['content'] = 'ANSIBLE_NO_LOG_VALUE' + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/acme.py b/ansible_collections/community/crypto/plugins/doc_fragments/acme.py new file mode 100644 index 000000000..a50cedd69 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/doc_fragments/acme.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +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)." + - "So far, the ACME modules have only been tested by the developers against + Let's Encrypt (staging and production), Buypass (staging and production), ZeroSSL (production), + and L(Pebble testing server,https://github.com/letsencrypt/Pebble). We have got + community feedback that they also work with Sectigo ACME Service for InCommon. + If you experience problems with another ACME server, please + L(create an issue,https://github.com/ansible-collections/community.crypto/issues/new/choose) + to help us supporting it. Feedback that an ACME server not mentioned does work + is also appreciated." +requirements: + - either openssl or L(cryptography,https://cryptography.io/) >= 1.5 + - ipaddress +options: + account_key_src: + description: + - "Path to a file containing the ACME account RSA or Elliptic Curve + key." + - "Private keys can be created with the + M(community.crypto.openssl_privatekey) or M(community.crypto.openssl_privatekey_pipe) + modules. If the requisite (cryptography) is not available, + keys can also be created directly with the C(openssl) command line tool: + 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." + - "B(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 + account_key_passphrase: + description: + - Phassphrase to use to decode the account key. + - "B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend." + type: str + version_added: 1.6.0 + 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 + acme_version: + description: + - "The ACME version of the endpoint." + - "Must be C(1) for the classic Let's Encrypt and Buypass ACME endpoints, + or C(2) for standardized ACME v2 endpoints." + - "The value C(1) is deprecated since community.crypto 2.0.0 and will be + removed from community.crypto 3.0.0." + required: true + type: int + choices: [ 1, 2 ] + acme_directory: + description: + - "The ACME directory to use. This is the entry point URL to access + the ACME 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." + - "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 B(Let's Encrypt), the production directory URL for ACME v2 is + U(https://acme-v02.api.letsencrypt.org/directory)." + - "For B(Buypass), the production directory URL for ACME v2 and v1 is + U(https://api.buypass.com/acme/directory)." + - "For B(ZeroSSL), the production directory URL for ACME v2 is + U(https://acme.zerossl.com/v2/DV90)." + - "For B(Sectigo), the production directory URL for ACME v2 is + U(https://acme-qa.secure.trust-provider.com/v2/DV)." + - The notes for this module contain a list of ACME services this module has + been tested against. + required: true + type: str + validate_certs: + description: + - Whether calls to the ACME directory will validate TLS certificates. + - "B(Warning:) Should B(only ever) be set to C(false) for testing purposes, + for example when testing against a local Pebble server." + type: bool + default: true + 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 ] + request_timeout: + description: + - The time Ansible should wait for a response from the ACME API. + - This timeout is applied to all HTTP(S) requests (HEAD, GET, POST). + type: int + default: 10 + version_added: 2.3.0 +''' diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/attributes.py b/ansible_collections/community/crypto/plugins/doc_fragments/attributes.py new file mode 100644 index 000000000..11f6b5754 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/doc_fragments/attributes.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Standard documentation fragment + DOCUMENTATION = r''' +options: {} +attributes: + check_mode: + description: Can run in C(check_mode) and return changed status prediction without modifying target. + diff_mode: + description: Will return details on what has changed (or possibly needs changing in C(check_mode)), when in diff mode. +''' + + # Should be used together with the standard fragment + INFO_MODULE = r''' +options: {} +attributes: + check_mode: + support: full + details: + - This action does not modify state. + diff_mode: + support: N/A + details: + - This action does not modify state. +''' + + ACTIONGROUP_ACME = r''' +options: {} +attributes: + action_group: + description: Use C(group/acme) or C(group/community.crypto.acme) in C(module_defaults) to set defaults for this module. + support: full + membership: + - community.crypto.acme + - acme +''' + + FACTS = r''' +options: {} +attributes: + facts: + description: Action returns an C(ansible_facts) dictionary that will update existing host facts. +''' + + # Should be used together with the standard fragment and the FACTS fragment + FACTS_MODULE = r''' +options: {} +attributes: + check_mode: + support: full + details: + - This action does not modify state. + diff_mode: + support: N/A + details: + - This action does not modify state. + facts: + support: full +''' + + FILES = r''' +options: {} +attributes: + safe_file_operations: + description: Uses Ansible's strict file operation functions to ensure proper permissions and avoid data corruption. +''' + + FLOW = r''' +options: {} +attributes: + action: + description: Indicates this has a corresponding action plugin so some parts of the options can be executed on the controller. + async: + description: Supports being used with the C(async) keyword. +''' diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/ecs_credential.py b/ansible_collections/community/crypto/plugins/doc_fragments/ecs_credential.py new file mode 100644 index 000000000..0b6d40371 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/doc_fragments/ecs_credential.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# Copyright (c), Entrust Datacard Corporation, 2019 +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +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" +''' diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/module_certificate.py b/ansible_collections/community/crypto/plugins/doc_fragments/module_certificate.py new file mode 100644 index 000000000..648d4ce91 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/doc_fragments/module_certificate.py @@ -0,0 +1,404 @@ +# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +description: + - This module allows one to (re)generate OpenSSL certificates. + - It uses the cryptography python library to interact with OpenSSL. +requirements: + - cryptography >= 1.6 (if using C(selfsigned) or C(ownca) provider) +options: + force: + description: + - Generate the certificate, even if it already exists. + type: bool + default: false + + csr_path: + description: + - Path to the Certificate Signing Request (CSR) used to generate this certificate. + - This is mutually exclusive with I(csr_content). + type: path + csr_content: + description: + - Content of the Certificate Signing Request (CSR) used to generate this certificate. + - This is mutually exclusive with I(csr_path). + type: str + + privatekey_path: + description: + - Path to the private key to use when signing the certificate. + - This is mutually exclusive with I(privatekey_content). + type: path + privatekey_content: + description: + - Content of the private key to use when signing the certificate. + - This is mutually exclusive with I(privatekey_path). + type: str + + privatekey_passphrase: + description: + - The passphrase for the I(privatekey_path) resp. I(privatekey_content). + - This is required if the private key is password protected. + type: str + + ignore_timestamps: + description: + - Whether the "not before" and "not after" timestamps should be ignored for idempotency checks. + - It is better to keep the default value C(true) when using relative timestamps (like C(+0s) for now). + type: bool + default: true + version_added: 2.0.0 + + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography ] + +notes: + - All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern. + - Date specified should be UTC. Minutes and seconds are mandatory. + - For security reason, when you use C(ownca) provider, you should NOT run + M(community.crypto.x509_certificate) on a target machine, but on a dedicated CA machine. It + is recommended not to store the CA private key on the target machine. Once signed, the + certificate can be moved to the target machine. +seealso: +- module: community.crypto.openssl_csr +- module: community.crypto.openssl_csr_pipe +- module: community.crypto.openssl_dhparam +- module: community.crypto.openssl_pkcs12 +- module: community.crypto.openssl_privatekey +- module: community.crypto.openssl_privatekey_pipe +- module: community.crypto.openssl_publickey +''' + + BACKEND_ACME_DOCUMENTATION = r''' +description: + - This module allows one to (re)generate OpenSSL certificates. +requirements: + - acme-tiny >= 4.0.0 (if using the C(acme) provider) +options: + acme_accountkey_path: + description: + - The path to the accountkey for the C(acme) provider. + - This is only used by the C(acme) provider. + type: path + + acme_challenge_path: + description: + - The path to the ACME challenge directory that is served on U(http://<HOST>:80/.well-known/acme-challenge/) + - This is only used by the C(acme) provider. + type: path + + acme_chain: + description: + - Include the intermediate certificate to the generated certificate + - This is only used by the C(acme) provider. + - Note that this is only available for older versions of C(acme-tiny). + New versions include the chain automatically, and setting I(acme_chain) to C(true) results in an error. + type: bool + default: false + + acme_directory: + description: + - "The ACME directory to use. You can use any directory that supports the ACME protocol, such as Buypass or Let's Encrypt." + - "Let's Encrypt recommends using their staging server while developing jobs. U(https://letsencrypt.org/docs/staging-environment/)." + type: str + default: https://acme-v02.api.letsencrypt.org/directory +''' + + BACKEND_ENTRUST_DOCUMENTATION = r''' +options: + entrust_cert_type: + description: + - Specify the type of certificate requested. + - This is only used by the C(entrust) provider. + type: str + default: STANDARD_SSL + choices: [ 'STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', 'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT' ] + + entrust_requester_email: + description: + - The email of the requester of the certificate (for tracking purposes). + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: str + + entrust_requester_name: + description: + - The name of the requester of the certificate (for tracking purposes). + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: str + + entrust_requester_phone: + description: + - The phone number of the requester of the certificate (for tracking purposes). + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: str + + entrust_api_user: + description: + - The username for authentication to the Entrust Certificate Services (ECS) API. + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: str + + entrust_api_key: + description: + - The key (password) for authentication to the Entrust Certificate Services (ECS) API. + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: str + + entrust_api_client_cert_path: + description: + - The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API. + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: path + + entrust_api_client_cert_key_path: + description: + - The path to the private key of the client certificate used to authenticate to the Entrust Certificate Services (ECS) API. + - This is only used by the C(entrust) provider. + - This is required if the provider is C(entrust). + type: path + + entrust_not_after: + description: + - The point in time at which the certificate stops being valid. + - Time can be specified either as relative time or as an absolute timestamp. + - A valid absolute time format is C(ASN.1 TIME) such as C(2019-06-18). + - A valid relative time format is C([+-]timespec) where timespec can be an integer + C([w | d | h | m | s]), such as C(+365d) or C(+32w1d2h)). + - Time will always be interpreted as UTC. + - Note that only the date (day, month, year) is supported for specifying the expiry date of the issued certificate. + - The full date-time is adjusted to EST (GMT -5:00) before issuance, which may result in a certificate with an expiration date one day + earlier than expected if a relative time is used. + - The minimum certificate lifetime is 90 days, and maximum is three years. + - If this value is not specified, the certificate will stop being valid 365 days the date of issue. + - This is only used by the C(entrust) provider. + - Please note that this value is B(not) covered by the I(ignore_timestamps) option. + type: str + default: +365d + + 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. + - This is only used by the C(entrust) provider. + type: path + default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml +''' + + BACKEND_OWNCA_DOCUMENTATION = r''' +description: + - The C(ownca) provider is intended for generating an OpenSSL certificate signed with your own + CA (Certificate Authority) certificate (self-signed certificate). +options: + ownca_path: + description: + - Remote absolute path of the CA (Certificate Authority) certificate. + - This is only used by the C(ownca) provider. + - This is mutually exclusive with I(ownca_content). + type: path + ownca_content: + description: + - Content of the CA (Certificate Authority) certificate. + - This is only used by the C(ownca) provider. + - This is mutually exclusive with I(ownca_path). + type: str + + ownca_privatekey_path: + description: + - Path to the CA (Certificate Authority) private key to use when signing the certificate. + - This is only used by the C(ownca) provider. + - This is mutually exclusive with I(ownca_privatekey_content). + type: path + ownca_privatekey_content: + description: + - Content of the CA (Certificate Authority) private key to use when signing the certificate. + - This is only used by the C(ownca) provider. + - This is mutually exclusive with I(ownca_privatekey_path). + type: str + + ownca_privatekey_passphrase: + description: + - The passphrase for the I(ownca_privatekey_path) resp. I(ownca_privatekey_content). + - This is only used by the C(ownca) provider. + type: str + + ownca_digest: + description: + - The digest algorithm to be used for the C(ownca) certificate. + - This is only used by the C(ownca) provider. + type: str + default: sha256 + + ownca_version: + description: + - The version of the C(ownca) certificate. + - Nowadays it should almost always be C(3). + - This is only used by the C(ownca) provider. + type: int + default: 3 + + ownca_not_before: + 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 format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (for example C(+32w1d2h)). + - If this value is not specified, the certificate will start being valid from now. + - Note that this value is B(not used to determine whether an existing certificate should be regenerated). + This can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should + avoid relative timestamps when setting I(ignore_timestamps=false). + - This is only used by the C(ownca) provider. + type: str + default: +0s + + ownca_not_after: + description: + - The point in time at which the certificate stops being valid. + - Time can be specified either as relative time or as absolute timestamp. + - Time will always be interpreted as UTC. + - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (for example C(+32w1d2h)). + - If this value is not specified, the certificate will stop being valid 10 years from now. + - Note that this value is B(not used to determine whether an existing certificate should be regenerated). + This can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should + avoid relative timestamps when setting I(ignore_timestamps=false). + - This is only used by the C(ownca) provider. + - On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer. + Please see U(https://support.apple.com/en-us/HT210176) for more details. + type: str + default: +3650d + + ownca_create_subject_key_identifier: + description: + - Whether to create the Subject Key Identifier (SKI) from the public key. + - A value of C(create_if_not_provided) (default) only creates a SKI when the CSR does not + provide one. + - A value of C(always_create) always creates a SKI. If the CSR provides one, that one is + ignored. + - A value of C(never_create) never creates a SKI. If the CSR provides one, that one is used. + - This is only used by the C(ownca) provider. + - Note that this is only supported if the C(cryptography) backend is used! + type: str + choices: [create_if_not_provided, always_create, never_create] + default: create_if_not_provided + + ownca_create_authority_key_identifier: + description: + - Create a Authority Key Identifier from the CA's certificate. If the CSR provided + a authority key identifier, it is ignored. + - The Authority Key Identifier is generated from the CA certificate's Subject Key Identifier, + if available. If it is not available, the CA certificate's public key will be used. + - This is only used by the C(ownca) provider. + - Note that this is only supported if the C(cryptography) backend is used! + type: bool + default: true +''' + + BACKEND_SELFSIGNED_DOCUMENTATION = r''' +notes: + - For the C(selfsigned) provider, I(csr_path) and I(csr_content) are optional. If not provided, a + certificate without any information (Subject, Subject Alternative Names, Key Usage, etc.) is created. + +options: + # NOTE: descriptions in options are overwritten, not appended. For that reason, the texts provided + # here for csr_path and csr_content are not visible to the user. That's why this information is + # added to the notes (see above). + + # csr_path: + # description: + # - This is optional for the C(selfsigned) provider. If not provided, a certificate + # without any information (Subject, Subject Alternative Names, Key Usage, etc.) is + # created. + + # csr_content: + # description: + # - This is optional for the C(selfsigned) provider. If not provided, a certificate + # without any information (Subject, Subject Alternative Names, Key Usage, etc.) is + # created. + + selfsigned_version: + description: + - Version of the C(selfsigned) certificate. + - Nowadays it should almost always be C(3). + - This is only used by the C(selfsigned) provider. + type: int + default: 3 + + selfsigned_digest: + description: + - Digest algorithm to be used when self-signing the certificate. + - This is only used by the C(selfsigned) provider. + type: str + default: sha256 + + selfsigned_not_before: + 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 format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (for example C(+32w1d2h)). + - If this value is not specified, the certificate will start being valid from now. + - Note that this value is B(not used to determine whether an existing certificate should be regenerated). + This can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should + avoid relative timestamps when setting I(ignore_timestamps=false). + - This is only used by the C(selfsigned) provider. + type: str + default: +0s + aliases: [ selfsigned_notBefore ] + + selfsigned_not_after: + description: + - The point in time at which the certificate stops being valid. + - Time can be specified either as relative time or as absolute timestamp. + - Time will always be interpreted as UTC. + - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer + + C([w | d | h | m | s]) (for example C(+32w1d2h)). + - If this value is not specified, the certificate will stop being valid 10 years from now. + - Note that this value is B(not used to determine whether an existing certificate should be regenerated). + This can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should + avoid relative timestamps when setting I(ignore_timestamps=false). + - This is only used by the C(selfsigned) provider. + - On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer. + Please see U(https://support.apple.com/en-us/HT210176) for more details. + type: str + default: +3650d + aliases: [ selfsigned_notAfter ] + + selfsigned_create_subject_key_identifier: + description: + - Whether to create the Subject Key Identifier (SKI) from the public key. + - A value of C(create_if_not_provided) (default) only creates a SKI when the CSR does not + provide one. + - A value of C(always_create) always creates a SKI. If the CSR provides one, that one is + ignored. + - A value of C(never_create) never creates a SKI. If the CSR provides one, that one is used. + - This is only used by the C(selfsigned) provider. + - Note that this is only supported if the C(cryptography) backend is used! + type: str + choices: [create_if_not_provided, always_create, never_create] + default: create_if_not_provided +''' diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/module_csr.py b/ansible_collections/community/crypto/plugins/doc_fragments/module_csr.py new file mode 100644 index 000000000..81c4318a4 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/doc_fragments/module_csr.py @@ -0,0 +1,325 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2017, Yanis Guenane <yanis+ansible@guenane.org> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +description: + - This module allows one to (re)generate OpenSSL certificate signing requests. + - This module supports the subjectAltName, keyUsage, extendedKeyUsage, basicConstraints and OCSP Must Staple + extensions. +requirements: + - cryptography >= 1.3 +options: + digest: + description: + - The digest used when signing the certificate signing request with the private key. + type: str + default: sha256 + privatekey_path: + description: + - The path to the private key to use when signing the certificate signing request. + - 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 private key to use when signing the certificate signing request. + - 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 private key. + - This is required if the private key is password protected. + type: str + version: + description: + - The version of the certificate signing request. + - "The only allowed value according to L(RFC 2986,https://tools.ietf.org/html/rfc2986#section-4.1) + is 1." + - This option no longer accepts unsupported values since community.crypto 2.0.0. + type: int + default: 1 + choices: + - 1 + subject: + description: + - Key/value pairs that will be present in the subject name field of the certificate signing request. + - If you need to specify more than one value with the same key, use a list as value. + - If the order of the components is important, use I(subject_ordered). + - Mutually exclusive with I(subject_ordered). + type: dict + subject_ordered: + description: + - A list of dictionaries, where every dictionary must contain one key/value pair. This key/value pair + will be present in the subject name field of the certificate signing request. + - If you want to specify more than one value with the same key in a row, you can use a list as value. + - Mutually exclusive with I(subject), and any other subject field option, such as I(country_name), + I(state_or_province_name), I(locality_name), I(organization_name), I(organizational_unit_name), + I(common_name), or I(email_address). + type: list + elements: dict + version_added: 2.0.0 + country_name: + description: + - The countryName field of the certificate signing request subject. + type: str + aliases: [ C, countryName ] + state_or_province_name: + description: + - The stateOrProvinceName field of the certificate signing request subject. + type: str + aliases: [ ST, stateOrProvinceName ] + locality_name: + description: + - The localityName field of the certificate signing request subject. + type: str + aliases: [ L, localityName ] + organization_name: + description: + - The organizationName field of the certificate signing request subject. + type: str + aliases: [ O, organizationName ] + organizational_unit_name: + description: + - The organizationalUnitName field of the certificate signing request subject. + type: str + aliases: [ OU, organizationalUnitName ] + common_name: + description: + - The commonName field of the certificate signing request subject. + type: str + aliases: [ CN, commonName ] + email_address: + description: + - The emailAddress field of the certificate signing request subject. + type: str + aliases: [ E, emailAddress ] + subject_alt_name: + description: + - Subject Alternative Name (SAN) extension to attach to the certificate signing request. + - Values must be prefixed by their options. (These are C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), + C(otherName), and the ones specific to your CA). + - Note that if no SAN is specified, but a common name, the common + name will be added as a SAN except if C(useCommonNameForSAN) is + set to I(false). + - More at U(https://tools.ietf.org/html/rfc5280#section-4.2.1.6). + type: list + elements: str + aliases: [ subjectAltName ] + subject_alt_name_critical: + description: + - Should the subjectAltName extension be considered as critical. + type: bool + default: false + aliases: [ subjectAltName_critical ] + use_common_name_for_san: + description: + - If set to C(true), the module will fill the common name in for + C(subject_alt_name) with C(DNS:) prefix if no SAN is specified. + type: bool + default: true + aliases: [ useCommonNameForSAN ] + key_usage: + description: + - This defines the purpose (for example encipherment, signature, certificate signing) + of the key contained in the certificate. + type: list + elements: str + aliases: [ keyUsage ] + key_usage_critical: + description: + - Should the keyUsage extension be considered as critical. + type: bool + default: false + aliases: [ keyUsage_critical ] + extended_key_usage: + description: + - Additional restrictions (for example client authentication, server authentication) + on the allowed purposes for which the public key may be used. + type: list + elements: str + aliases: [ extKeyUsage, extendedKeyUsage ] + extended_key_usage_critical: + description: + - Should the extkeyUsage extension be considered as critical. + type: bool + default: false + aliases: [ extKeyUsage_critical, extendedKeyUsage_critical ] + basic_constraints: + description: + - Indicates basic constraints, such as if the certificate is a CA. + type: list + elements: str + aliases: [ basicConstraints ] + basic_constraints_critical: + description: + - Should the basicConstraints extension be considered as critical. + type: bool + default: false + aliases: [ basicConstraints_critical ] + ocsp_must_staple: + description: + - Indicates that the certificate should contain the OCSP Must Staple + extension (U(https://tools.ietf.org/html/rfc7633)). + type: bool + default: false + aliases: [ ocspMustStaple ] + ocsp_must_staple_critical: + description: + - Should the OCSP Must Staple extension be considered as critical. + - Note that according to the RFC, this extension should not be marked + as critical, as old clients not knowing about OCSP Must Staple + are required to reject such certificates + (see U(https://tools.ietf.org/html/rfc7633#section-4)). + type: bool + default: false + aliases: [ ocspMustStaple_critical ] + name_constraints_permitted: + description: + - For CA certificates, this specifies a list of identifiers which describe + subtrees of names that this CA is allowed to issue certificates for. + - Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), + C(otherName) and the ones specific to your CA). + type: list + elements: str + name_constraints_excluded: + description: + - For CA certificates, this specifies a list of identifiers which describe + subtrees of names that this CA is B(not) allowed to issue certificates for. + - Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), + C(otherName) and the ones specific to your CA). + type: list + elements: str + name_constraints_critical: + description: + - Should the Name Constraints extension be considered as critical. + type: bool + default: false + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography ] + create_subject_key_identifier: + description: + - Create the Subject Key Identifier from the public key. + - "Please note that commercial CAs can ignore the value, respectively use a value of + their own choice instead. Specifying this option is mostly useful for self-signed + certificates or for own CAs." + - Note that this is only supported if the C(cryptography) backend is used! + type: bool + default: false + subject_key_identifier: + description: + - The subject key identifier as a hex string, where two bytes are separated by colons. + - "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)" + - "Please note that commercial CAs ignore this value, respectively use a value of their + own choice. Specifying this option is mostly useful for self-signed certificates + or for own CAs." + - Note that this option can only be used if I(create_subject_key_identifier) is C(false). + - Note that this is only supported if the C(cryptography) backend is used! + type: str + authority_key_identifier: + description: + - The authority key identifier as a hex string, where two bytes are separated by colons. + - "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)" + - "Please note that commercial CAs ignore this value, respectively use a value of their + own choice. Specifying this option is mostly useful for self-signed certificates + or for own CAs." + - Note that this is only supported if the C(cryptography) backend is used! + - The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier), + I(authority_cert_issuer) and I(authority_cert_serial_number) is specified. + type: str + authority_cert_issuer: + description: + - Names that will be present in the authority cert issuer field of the certificate signing request. + - Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), + C(otherName) and the ones specific to your CA) + - "Example: C(DNS:ca.example.org)" + - If specified, I(authority_cert_serial_number) must also be specified. + - "Please note that commercial CAs ignore this value, respectively use a value of their + own choice. Specifying this option is mostly useful for self-signed certificates + or for own CAs." + - Note that this is only supported if the C(cryptography) backend is used! + - The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier), + I(authority_cert_issuer) and I(authority_cert_serial_number) is specified. + type: list + elements: str + authority_cert_serial_number: + description: + - The authority cert serial number. + - If specified, I(authority_cert_issuer) must also be specified. + - Note that this is only supported if the C(cryptography) backend is used! + - "Please note that commercial CAs ignore this value, respectively use a value of their + own choice. Specifying this option is mostly useful for self-signed certificates + or for own CAs." + - The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier), + I(authority_cert_issuer) and I(authority_cert_serial_number) is specified. + type: int + crl_distribution_points: + description: + - Allows to specify one or multiple CRL distribution points. + - Only supported by the C(cryptography) backend. + type: list + elements: dict + suboptions: + full_name: + description: + - Describes how the CRL can be retrieved. + - Mutually exclusive with I(relative_name). + - "Example: C(URI:https://ca.example.com/revocations.crl)." + type: list + elements: str + relative_name: + description: + - Describes how the CRL can be retrieved relative to the CRL issuer. + - Mutually exclusive with I(full_name). + - "Example: C(/CN=example.com)." + - Can only be used when cryptography >= 1.6 is installed. + type: list + elements: str + crl_issuer: + description: + - Information about the issuer of the CRL. + type: list + elements: str + reasons: + description: + - List of reasons that this distribution point can be used for when performing revocation checks. + type: list + elements: str + choices: + - key_compromise + - ca_compromise + - affiliation_changed + - superseded + - cessation_of_operation + - certificate_hold + - privilege_withdrawn + - aa_compromise + version_added: 1.4.0 +notes: + - If the certificate signing request already exists it will be checked whether subjectAltName, + keyUsage, extendedKeyUsage and basicConstraints only contain the requested values, whether + OCSP Must Staple is as requested, and if the request was signed by the given private key. +seealso: +- module: community.crypto.x509_certificate +- module: community.crypto.x509_certificate_pipe +- module: community.crypto.openssl_dhparam +- module: community.crypto.openssl_pkcs12 +- module: community.crypto.openssl_privatekey +- module: community.crypto.openssl_privatekey_pipe +- module: community.crypto.openssl_publickey +- module: community.crypto.openssl_csr_info +''' diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey.py b/ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey.py new file mode 100644 index 000000000..a27b26c7d --- /dev/null +++ b/ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +description: + - 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 do not 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 B(overwrite your private key), + consider using the I(backup) option." +requirements: + - cryptography >= 1.2.3 (older versions might work as well) +options: + 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). + - Please note that all curves except C(secp224r1), C(secp256k1), C(secp256r1), C(secp384r1) and C(secp521r1) + are discouraged for new private keys. + type: str + choices: + - secp224r1 + - secp256k1 + - secp256r1 + - secp384r1 + - secp521r1 + - secp192r1 + - brainpoolP256r1 + - brainpoolP384r1 + - brainpoolP512r1 + - sect163k1 + - sect163r2 + - sect233k1 + - sect233r1 + - sect283k1 + - sect283r1 + - sect409k1 + - sect409r1 + - sect571k1 + - sect571r1 + passphrase: + description: + - The passphrase for the private key. + type: str + cipher: + description: + - The cipher to encrypt the private key. Must be C(auto). + type: str + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography ] + 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 format 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 B(regenerated) by default. + To change this behavior, use the I(format_mismatch) option. + type: str + default: auto_ignore + choices: [ pkcs1, pkcs8, raw, auto, auto_ignore ] + 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 ] + 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 does not 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 + is not 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(true). + - 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 +seealso: +- module: community.crypto.x509_certificate +- module: community.crypto.x509_certificate_pipe +- module: community.crypto.openssl_csr +- module: community.crypto.openssl_csr_pipe +- module: community.crypto.openssl_dhparam +- module: community.crypto.openssl_pkcs12 +- module: community.crypto.openssl_publickey +''' diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey_convert.py b/ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey_convert.py new file mode 100644 index 000000000..f1c6f70ec --- /dev/null +++ b/ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey_convert.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +requirements: + - cryptography >= 1.2.3 (older versions might work as well) +options: + src_path: + description: + - Name of the file containing the OpenSSL private key to convert. + - Exactly one of I(src_path) or I(src_content) must be specified. + type: path + src_content: + description: + - The content of the file containing the OpenSSL private key to convert. + - Exactly one of I(src_path) or I(src_content) must be specified. + type: str + src_passphrase: + description: + - The passphrase for the private key to load. + type: str + dest_passphrase: + description: + - The passphrase for the private key to store. + type: str + format: + description: + - Determines which format the destination private key should be written in. + - Please note that not every key can be exported in any format, and that not every + format supports encryption. + type: str + choices: [ pkcs1, pkcs8, raw ] + required: true +seealso: + - module: community.crypto.openssl_privatekey + - module: community.crypto.openssl_privatekey_pipe + - module: community.crypto.openssl_publickey +''' diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/name_encoding.py b/ansible_collections/community/crypto/plugins/doc_fragments/name_encoding.py new file mode 100644 index 000000000..fec94380d --- /dev/null +++ b/ansible_collections/community/crypto/plugins/doc_fragments/name_encoding.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class ModuleDocFragment(object): + DOCUMENTATION = r''' +options: + name_encoding: + description: + - How to encode names (DNS names, URIs, email addresses) in return values. + - C(ignore) will use the encoding returned by the backend. + - C(idna) will convert all labels of domain names to IDNA encoding. + IDNA2008 will be preferred, and IDNA2003 will be used if IDNA2008 encoding fails. + - C(unicode) will convert all labels of domain names to Unicode. + IDNA2008 will be preferred, and IDNA2003 will be used if IDNA2008 decoding fails. + - B(Note) that C(idna) and C(unicode) require the L(idna Python library,https://pypi.org/project/idna/) to be installed. + type: str + default: ignore + choices: + - ignore + - idna + - unicode +requirements: + - If I(name_encoding) is set to another value than C(ignore), the L(idna Python library,https://pypi.org/project/idna/) needs to be installed. +''' diff --git a/ansible_collections/community/crypto/plugins/filter/openssl_csr_info.py b/ansible_collections/community/crypto/plugins/filter/openssl_csr_info.py new file mode 100644 index 000000000..851dfe2a4 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/filter/openssl_csr_info.py @@ -0,0 +1,313 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +name: openssl_csr_info +short_description: Retrieve information from OpenSSL Certificate Signing Requests (CSR) +version_added: 2.10.0 +author: + - Felix Fontein (@felixfontein) +description: + - Provided an OpenSSL Certificate Signing Requests (CSR), retrieve information. + - This is a filter version of the M(community.crypto.openssl_csr_info) module. +options: + _input: + description: + - The content of the OpenSSL CSR. + type: string + required: true +extends_documentation_fragment: + - community.crypto.name_encoding +seealso: + - module: community.crypto.openssl_csr_info +''' + +EXAMPLES = ''' +- name: Show the Subject Alt Names of the CSR + ansible.builtin.debug: + msg: >- + {{ + ( + lookup('ansible.builtin.file', '/path/to/cert.csr') + | community.crypto.openssl_csr_info + ).subject_alt_name | join(', ') + }} +''' + +RETURN = ''' +_value: + description: + - Information on the certificate. + type: dict + contains: + signature_valid: + description: + - Whether the CSR's signature is valid. + - In case the check returns C(false), 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. + - B(Note) that depending on the C(cryptography) version used, it is + not possible to extract the ASN.1 content of the extension, but only + to provide the re-encoded content of the extension in case it was + parsed by C(cryptography). This should usually result in exactly the + same value, except if the original extension value was malformed. + 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. + - See I(name_encoding) for how IDNs are handled. + 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(true) 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 + name_constraints_permitted: + description: List of permitted subtrees to sign certificates for. + returned: success + type: list + elements: str + sample: ['email:.somedomain.com'] + name_constraints_excluded: + description: + - List of excluded subtrees the CA cannot sign certificates for. + - Is C(none) if extension is not present. + - See I(name_encoding) for how IDNs are handled. + returned: success + type: list + elements: str + sample: ['email:.com'] + name_constraints_critical: + description: + - Whether the C(name_constraints) extension is critical. + - Is C(none) if extension is not present. + 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"]] + public_key: + description: CSR's public key in PEM format + returned: success + type: str + sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..." + public_key_type: + description: + - The CSR's public 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_key_data: + description: + - Public key data. Depends on the public key's type. + returned: success + type: dict + contains: + size: + description: + - Bit size of modulus (RSA) or prime number (DSA). + type: int + returned: When C(public_key_type=RSA) or C(public_key_type=DSA) + modulus: + description: + - The RSA key's modulus. + type: int + returned: When C(public_key_type=RSA) + exponent: + description: + - The RSA key's public exponent. + type: int + returned: When C(public_key_type=RSA) + p: + description: + - The C(p) value for DSA. + - This is the prime modulus upon which arithmetic takes place. + type: int + returned: When C(public_key_type=DSA) + q: + description: + - The C(q) value for DSA. + - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the + multiplicative group of the prime field used. + type: int + returned: When C(public_key_type=DSA) + g: + description: + - The C(g) value for DSA. + - This is the element spanning the subgroup of the multiplicative group of the prime field used. + type: int + returned: When C(public_key_type=DSA) + curve: + description: + - The curve's name for ECC. + type: str + returned: When C(public_key_type=ECC) + exponent_size: + description: + - The maximum number of bits of a private key. This is basically the bit size of the subgroup used. + type: int + returned: When C(public_key_type=ECC) + x: + description: + - The C(x) coordinate for the public point on the elliptic curve. + type: int + returned: When C(public_key_type=ECC) + y: + description: + - For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. + - For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. + type: int + returned: When C(public_key_type=DSA) or C(public_key_type=ECC) + 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 + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' + 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 + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' + 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. + - See I(name_encoding) for how IDNs are handled. + returned: success + type: list + elements: str + sample: ["DNS:www.ansible.com", "IP:1.2.3.4"] + 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 + type: int + sample: 12345 +''' + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.six import string_types +from ansible.module_utils.common.text.converters import to_bytes, to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import ( + get_csr_info, +) + +from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock + + +def openssl_csr_info_filter(data, name_encoding='ignore'): + '''Extract information from X.509 PEM certificate.''' + if not isinstance(data, string_types): + raise AnsibleFilterError('The community.crypto.openssl_csr_info input must be a text type, not %s' % type(data)) + if not isinstance(name_encoding, string_types): + raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding)) + name_encoding = to_native(name_encoding) + if name_encoding not in ('ignore', 'idna', 'unicode'): + raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding) + + module = FilterModuleMock({'name_encoding': name_encoding}) + try: + return get_csr_info(module, 'cryptography', content=to_bytes(data), validate_signature=True) + except OpenSSLObjectError as exc: + raise AnsibleFilterError(to_native(exc)) + + +class FilterModule(object): + '''Ansible jinja2 filters''' + + def filters(self): + return { + 'openssl_csr_info': openssl_csr_info_filter, + } diff --git a/ansible_collections/community/crypto/plugins/filter/openssl_privatekey_info.py b/ansible_collections/community/crypto/plugins/filter/openssl_privatekey_info.py new file mode 100644 index 000000000..16dfd8597 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/filter/openssl_privatekey_info.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +name: openssl_privatekey_info +short_description: Retrieve information from OpenSSL private keys +version_added: 2.10.0 +author: + - Felix Fontein (@felixfontein) +description: + - Provided an OpenSSL private keys, retrieve information. + - This is a filter version of the M(community.crypto.openssl_privatekey_info) module. +options: + _input: + description: + - The content of the OpenSSL private key. + type: string + required: true + 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(true) when you want private information about this key to + be extracted. + - "B(WARNING:) you have to make sure that private key data is not accidentally logged!" + type: bool + default: false +extends_documentation_fragment: + - community.crypto.name_encoding +seealso: + - module: community.crypto.openssl_privatekey_info +''' + +EXAMPLES = ''' +- name: Show the Subject Alt Names of the CSR + ansible.builtin.debug: + msg: >- + {{ + ( + lookup('ansible.builtin.file', '/path/to/cert.csr') + | community.crypto.openssl_privatekey_info + ).subject_alt_name | join(', ') + }} +''' + +RETURN = ''' +_value: + description: + - Information on the certificate. + type: dict + contains: + 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 + contains: + size: + description: + - Bit size of modulus (RSA) or prime number (DSA). + type: int + returned: When C(type=RSA) or C(type=DSA) + modulus: + description: + - The RSA key's modulus. + type: int + returned: When C(type=RSA) + exponent: + description: + - The RSA key's public exponent. + type: int + returned: When C(type=RSA) + p: + description: + - The C(p) value for DSA. + - This is the prime modulus upon which arithmetic takes place. + type: int + returned: When C(type=DSA) + q: + description: + - The C(q) value for DSA. + - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the + multiplicative group of the prime field used. + type: int + returned: When C(type=DSA) + g: + description: + - The C(g) value for DSA. + - This is the element spanning the subgroup of the multiplicative group of the prime field used. + type: int + returned: When C(type=DSA) + curve: + description: + - The curve's name for ECC. + type: str + returned: When C(type=ECC) + exponent_size: + description: + - The maximum number of bits of a private key. This is basically the bit size of the subgroup used. + type: int + returned: When C(type=ECC) + x: + description: + - The C(x) coordinate for the public point on the elliptic curve. + type: int + returned: When C(type=ECC) + y: + description: + - For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. + - For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. + type: int + returned: When C(type=DSA) or C(type=ECC) + private_data: + description: + - Private key data. Depends on key type. + returned: success and when I(return_private_key_data) is set to C(true) + type: dict +''' + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.six import string_types +from ansible.module_utils.common.text.converters import to_bytes, to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import ( + PrivateKeyParseError, + get_privatekey_info, +) + +from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock + + +def openssl_privatekey_info_filter(data, passphrase=None, return_private_key_data=False): + '''Extract information from X.509 PEM certificate.''' + if not isinstance(data, string_types): + raise AnsibleFilterError('The community.crypto.openssl_privatekey_info input must be a text type, not %s' % type(data)) + if passphrase is not None and not isinstance(passphrase, string_types): + raise AnsibleFilterError('The passphrase option must be a text type, not %s' % type(passphrase)) + if not isinstance(return_private_key_data, bool): + raise AnsibleFilterError('The return_private_key_data option must be a boolean, not %s' % type(return_private_key_data)) + + module = FilterModuleMock({}) + try: + result = get_privatekey_info(module, 'cryptography', content=to_bytes(data), passphrase=passphrase, return_private_key_data=return_private_key_data) + result.pop('can_parse_key', None) + result.pop('key_is_consistent', None) + return result + except PrivateKeyParseError as exc: + raise AnsibleFilterError(exc.error_message) + except OpenSSLObjectError as exc: + raise AnsibleFilterError(to_native(exc)) + + +class FilterModule(object): + '''Ansible jinja2 filters''' + + def filters(self): + return { + 'openssl_privatekey_info': openssl_privatekey_info_filter, + } diff --git a/ansible_collections/community/crypto/plugins/filter/openssl_publickey_info.py b/ansible_collections/community/crypto/plugins/filter/openssl_publickey_info.py new file mode 100644 index 000000000..f41af1c79 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/filter/openssl_publickey_info.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +name: openssl_publickey_info +short_description: Retrieve information from OpenSSL public keys in PEM format +version_added: 2.10.0 +author: + - Felix Fontein (@felixfontein) +description: + - Provided a public key in OpenSSL PEM format, retrieve information. + - This is a filter version of the M(community.crypto.openssl_publickey_info) module. +options: + _input: + description: + - The content of the OpenSSL PEM public key. + type: string + required: true +seealso: + - module: community.crypto.openssl_publickey_info +''' + +EXAMPLES = ''' +- name: Show the type of a public key + ansible.builtin.debug: + msg: >- + {{ + ( + lookup('ansible.builtin.file', '/path/to/public-key.pem') + | community.crypto.openssl_publickey_info + ).type + }} +''' + +RETURN = ''' +_value: + description: + - Information on the public key. + type: dict + contains: + fingerprints: + description: + - Fingerprints of 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 + contains: + size: + description: + - Bit size of modulus (RSA) or prime number (DSA). + type: int + returned: When C(type=RSA) or C(type=DSA) + modulus: + description: + - The RSA key's modulus. + type: int + returned: When C(type=RSA) + exponent: + description: + - The RSA key's public exponent. + type: int + returned: When C(type=RSA) + p: + description: + - The C(p) value for DSA. + - This is the prime modulus upon which arithmetic takes place. + type: int + returned: When C(type=DSA) + q: + description: + - The C(q) value for DSA. + - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the + multiplicative group of the prime field used. + type: int + returned: When C(type=DSA) + g: + description: + - The C(g) value for DSA. + - This is the element spanning the subgroup of the multiplicative group of the prime field used. + type: int + returned: When C(type=DSA) + curve: + description: + - The curve's name for ECC. + type: str + returned: When C(type=ECC) + exponent_size: + description: + - The maximum number of bits of a private key. This is basically the bit size of the subgroup used. + type: int + returned: When C(type=ECC) + x: + description: + - The C(x) coordinate for the public point on the elliptic curve. + type: int + returned: When C(type=ECC) + y: + description: + - For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. + - For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. + type: int + returned: When C(type=DSA) or C(type=ECC) +''' + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.six import string_types +from ansible.module_utils.common.text.converters import to_bytes, to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import ( + PublicKeyParseError, + get_publickey_info, +) + +from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock + + +def openssl_publickey_info_filter(data): + '''Extract information from OpenSSL PEM public key.''' + if not isinstance(data, string_types): + raise AnsibleFilterError('The community.crypto.openssl_publickey_info input must be a text type, not %s' % type(data)) + + module = FilterModuleMock({}) + try: + return get_publickey_info(module, 'cryptography', content=to_bytes(data)) + except PublicKeyParseError as exc: + raise AnsibleFilterError(exc.error_message) + except OpenSSLObjectError as exc: + raise AnsibleFilterError(to_native(exc)) + + +class FilterModule(object): + '''Ansible jinja2 filters''' + + def filters(self): + return { + 'openssl_publickey_info': openssl_publickey_info_filter, + } diff --git a/ansible_collections/community/crypto/plugins/filter/split_pem.py b/ansible_collections/community/crypto/plugins/filter/split_pem.py new file mode 100644 index 000000000..a58ce5060 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/filter/split_pem.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +name: split_pem +short_description: Split PEM file contents into multiple objects +version_added: 2.10.0 +author: + - Felix Fontein (@felixfontein) +description: + - Split PEM file contents into multiple PEM objects. Comments or invalid parts are ignored. +options: + _input: + description: + - The PEM contents to split. + type: string + required: true +''' + +EXAMPLES = ''' +- name: Print all CA certificates + ansible.builtin.debug: + msg: '{{ item }}' + loop: >- + {{ lookup('ansible.builtin.file', '/path/to/ca-bundle.pem') | community.crypto.split_pem }} +''' + +RETURN = ''' +_value: + description: + - A list of PEM file contents. + type: list + elements: string +''' + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.six import string_types +from ansible.module_utils.common.text.converters import to_text + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import split_pem_list + + +def split_pem_filter(data): + '''Split PEM file.''' + if not isinstance(data, string_types): + raise AnsibleFilterError('The community.crypto.split_pem input must be a text type, not %s' % type(data)) + + data = to_text(data) + return split_pem_list(data) + + +class FilterModule(object): + '''Ansible jinja2 filters''' + + def filters(self): + return { + 'split_pem': split_pem_filter, + } diff --git a/ansible_collections/community/crypto/plugins/filter/x509_certificate_info.py b/ansible_collections/community/crypto/plugins/filter/x509_certificate_info.py new file mode 100644 index 000000000..21aee98a9 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/filter/x509_certificate_info.py @@ -0,0 +1,346 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +name: x509_certificate_info +short_description: Retrieve information from X.509 certificates in PEM format +version_added: 2.10.0 +author: + - Felix Fontein (@felixfontein) +description: + - Provided a X.509 certificate in PEM format, retrieve information. + - This is a filter version of the M(community.crypto.x509_certificate_info) module. +options: + _input: + description: + - The content of the X.509 certificate in PEM format. + type: string + required: true +extends_documentation_fragment: + - community.crypto.name_encoding +seealso: + - module: community.crypto.x509_certificate_info +''' + +EXAMPLES = ''' +- name: Show the Subject Alt Names of the certificate + ansible.builtin.debug: + msg: >- + {{ + ( + lookup('ansible.builtin.file', '/path/to/cert.pem') + | community.crypto.x509_certificate_info + ).subject_alt_name | join(', ') + }} +''' + +RETURN = ''' +_value: + description: + - Information on the certificate. + type: dict + contains: + expired: + description: Whether the certificate is expired (in other words, 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. + - B(Note) that depending on the C(cryptography) version used, it is + not possible to extract the ASN.1 content of the extension, but only + to provide the re-encoded content of the extension in case it was + parsed by C(cryptography). This should usually result in exactly the + same value, except if the original extension value was malformed. + 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. + - See I(name_encoding) for how IDNs are handled. + 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(true) 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"]] + 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"]] + 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_type: + description: + - The certificate's public 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_key_data: + description: + - Public key data. Depends on the public key's type. + returned: success + type: dict + contains: + size: + description: + - Bit size of modulus (RSA) or prime number (DSA). + type: int + returned: When C(public_key_type=RSA) or C(public_key_type=DSA) + modulus: + description: + - The RSA key's modulus. + type: int + returned: When C(public_key_type=RSA) + exponent: + description: + - The RSA key's public exponent. + type: int + returned: When C(public_key_type=RSA) + p: + description: + - The C(p) value for DSA. + - This is the prime modulus upon which arithmetic takes place. + type: int + returned: When C(public_key_type=DSA) + q: + description: + - The C(q) value for DSA. + - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the + multiplicative group of the prime field used. + type: int + returned: When C(public_key_type=DSA) + g: + description: + - The C(g) value for DSA. + - This is the element spanning the subgroup of the multiplicative group of the prime field used. + type: int + returned: When C(public_key_type=DSA) + curve: + description: + - The curve's name for ECC. + type: str + returned: When C(public_key_type=ECC) + exponent_size: + description: + - The maximum number of bits of a private key. This is basically the bit size of the subgroup used. + type: int + returned: When C(public_key_type=ECC) + x: + description: + - The C(x) coordinate for the public point on the elliptic curve. + type: int + returned: When C(public_key_type=ECC) + y: + description: + - For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. + - For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. + type: int + returned: When C(public_key_type=DSA) or C(public_key_type=ECC) + 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..." + fingerprints: + description: + - Fingerprints of the DER-encoded form of the whole certificate. + - 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 + 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 + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' + 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 + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' + 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. + - See I(name_encoding) for how IDNs are handled. + returned: success + type: list + elements: str + sample: ["DNS:www.ansible.com", "IP:1.2.3.4"] + 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 + type: int + sample: 12345 + 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 + issuer_uri: + description: The Issuer URI, if included in the certificate. Will be + C(none) if no issuer URI is included. + returned: success + type: str +''' + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.six import string_types +from ansible.module_utils.common.text.converters import to_bytes, to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import ( + get_certificate_info, +) + +from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock + + +def x509_certificate_info_filter(data, name_encoding='ignore'): + '''Extract information from X.509 PEM certificate.''' + if not isinstance(data, string_types): + raise AnsibleFilterError('The community.crypto.x509_certificate_info input must be a text type, not %s' % type(data)) + if not isinstance(name_encoding, string_types): + raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding)) + name_encoding = to_native(name_encoding) + if name_encoding not in ('ignore', 'idna', 'unicode'): + raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding) + + module = FilterModuleMock({'name_encoding': name_encoding}) + try: + return get_certificate_info(module, 'cryptography', content=to_bytes(data)) + except OpenSSLObjectError as exc: + raise AnsibleFilterError(to_native(exc)) + + +class FilterModule(object): + '''Ansible jinja2 filters''' + + def filters(self): + return { + 'x509_certificate_info': x509_certificate_info_filter, + } diff --git a/ansible_collections/community/crypto/plugins/filter/x509_crl_info.py b/ansible_collections/community/crypto/plugins/filter/x509_crl_info.py new file mode 100644 index 000000000..11f61fd8a --- /dev/null +++ b/ansible_collections/community/crypto/plugins/filter/x509_crl_info.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +name: x509_crl_info +short_description: Retrieve information from X.509 CRLs in PEM format +version_added: 2.10.0 +author: + - Felix Fontein (@felixfontein) +description: + - Provided a X.509 crl in PEM format, retrieve information. + - This is a filter version of the M(community.crypto.x509_crl_info) module. +options: + _input: + description: + - The content of the X.509 CRL in PEM format. + type: string + required: true + list_revoked_certificates: + description: + - If set to C(false), the list of revoked certificates is not included in the result. + - This is useful when retrieving information on large CRL files. Enumerating all revoked + certificates can take some time, including serializing the result as JSON, sending it to + the Ansible controller, and decoding it again. + type: bool + default: true + version_added: 1.7.0 +extends_documentation_fragment: + - community.crypto.name_encoding +seealso: + - module: community.crypto.x509_crl_info +''' + +EXAMPLES = ''' +- name: Show the Organization Name of the CRL's subject + ansible.builtin.debug: + msg: >- + {{ + ( + lookup('ansible.builtin.file', '/path/to/cert.pem') + | community.crypto.x509_crl_info + ).issuer.organizationName + }} +''' + +RETURN = ''' +_value: + description: + - Information on the CRL. + type: dict + contains: + format: + description: + - Whether the CRL is in PEM format (C(pem)) or in DER format (C(der)). + returned: success + type: str + sample: pem + issuer: + description: + - The CRL's issuer. + - Note that for repeated values, only the last one will be returned. + - See I(name_encoding) for how IDNs are handled. + 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 if I(list_revoked_certificates=true) + 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. + - See I(name_encoding) for how IDNs are handled. + type: list + elements: str + sample: ["DNS:ca.example.org"] + issuer_critical: + description: Whether the certificate issuer extension is critical. + type: bool + sample: false + 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: false + 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: false +''' + +import base64 +import binascii + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.six import string_types +from ansible.module_utils.common.text.converters import to_bytes, to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + identify_pem_format, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import ( + get_crl_info, +) + +from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock + + +def x509_crl_info_filter(data, name_encoding='ignore', list_revoked_certificates=True): + '''Extract information from X.509 PEM certificate.''' + if not isinstance(data, string_types): + raise AnsibleFilterError('The community.crypto.x509_crl_info input must be a text type, not %s' % type(data)) + if not isinstance(name_encoding, string_types): + raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding)) + if not isinstance(list_revoked_certificates, bool): + raise AnsibleFilterError('The list_revoked_certificates option must be a boolean, not %s' % type(list_revoked_certificates)) + name_encoding = to_native(name_encoding) + if name_encoding not in ('ignore', 'idna', 'unicode'): + raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding) + + data = to_bytes(data) + if not identify_pem_format(data): + try: + data = base64.b64decode(to_native(data)) + except (binascii.Error, TypeError, ValueError, UnicodeEncodeError) as e: + pass + + module = FilterModuleMock({'name_encoding': name_encoding}) + try: + return get_crl_info(module, content=data, list_revoked_certificates=list_revoked_certificates) + except OpenSSLObjectError as exc: + raise AnsibleFilterError(to_native(exc)) + + +class FilterModule(object): + '''Ansible jinja2 filters''' + + def filters(self): + return { + 'x509_crl_info': x509_crl_info_filter, + } diff --git a/ansible_collections/community/crypto/plugins/module_utils/_version.py b/ansible_collections/community/crypto/plugins/module_utils/_version.py new file mode 100644 index 000000000..f7954074e --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/_version.py @@ -0,0 +1,345 @@ +# Vendored copy of distutils/version.py from CPython 3.9.5 +# +# Implements multiple version numbering conventions for the +# Python Module Distribution Utilities. +# +# Copyright (c) 2001-2022 Python Software Foundation. All rights reserved. +# PSF License (see LICENSES/PSF-2.0.txt or https://opensource.org/licenses/Python-2.0) +# SPDX-License-Identifier: PSF-2.0 +# + +"""Provides classes to represent module version numbers (one class for +each style of version numbering). There are currently two such classes +implemented: StrictVersion and LooseVersion. + +Every version number class implements the following interface: + * the 'parse' method takes a string and parses it to some internal + representation; if the string is an invalid version number, + 'parse' raises a ValueError exception + * the class constructor takes an optional string argument which, + if supplied, is passed to 'parse' + * __str__ reconstructs the string that was passed to 'parse' (or + an equivalent string -- ie. one that will generate an equivalent + version number instance) + * __repr__ generates Python code to recreate the version number instance + * _cmp compares the current instance with either another instance + of the same class or a string (which will be parsed to an instance + of the same class, thus must follow the same rules) +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re + +try: + RE_FLAGS = re.VERBOSE | re.ASCII +except AttributeError: + RE_FLAGS = re.VERBOSE + + +class Version: + """Abstract base class for version numbering classes. Just provides + constructor (__init__) and reproducer (__repr__), because those + seem to be the same for all version numbering classes; and route + rich comparisons to _cmp. + """ + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def __repr__(self): + return "%s ('%s')" % (self.__class__.__name__, str(self)) + + def __eq__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c == 0 + + def __lt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c < 0 + + def __le__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c <= 0 + + def __gt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c > 0 + + def __ge__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c >= 0 + + +# Interface for version-number classes -- must be implemented +# by the following classes (the concrete ones -- Version should +# be treated as an abstract class). +# __init__ (string) - create and take same action as 'parse' +# (string parameter is optional) +# parse (string) - convert a string representation to whatever +# internal representation is appropriate for +# this style of version numbering +# __str__ (self) - convert back to a string; should be very similar +# (if not identical to) the string supplied to parse +# __repr__ (self) - generate Python code to recreate +# the instance +# _cmp (self, other) - compare two version numbers ('other' may +# be an unparsed version string, or another +# instance of your version class) + + +class StrictVersion(Version): + """Version numbering for anal retentives and software idealists. + Implements the standard interface for version number classes as + described above. A version number consists of two or three + dot-separated numeric components, with an optional "pre-release" tag + on the end. The pre-release tag consists of the letter 'a' or 'b' + followed by a number. If the numeric components of two version + numbers are equal, then one with a pre-release tag will always + be deemed earlier (lesser) than one without. + + The following are valid version numbers (shown in the order that + would be obtained by sorting according to the supplied cmp function): + + 0.4 0.4.0 (these two are equivalent) + 0.4.1 + 0.5a1 + 0.5b3 + 0.5 + 0.9.6 + 1.0 + 1.0.4a3 + 1.0.4b1 + 1.0.4 + + The following are examples of invalid version numbers: + + 1 + 2.7.2.2 + 1.3.a4 + 1.3pl1 + 1.3c4 + + The rationale for this version numbering system will be explained + in the distutils documentation. + """ + + version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', + RE_FLAGS) + + def parse(self, vstring): + match = self.version_re.match(vstring) + if not match: + raise ValueError("invalid version number '%s'" % vstring) + + (major, minor, patch, prerelease, prerelease_num) = \ + match.group(1, 2, 4, 5, 6) + + if patch: + self.version = tuple(map(int, [major, minor, patch])) + else: + self.version = tuple(map(int, [major, minor])) + (0,) + + if prerelease: + self.prerelease = (prerelease[0], int(prerelease_num)) + else: + self.prerelease = None + + def __str__(self): + if self.version[2] == 0: + vstring = '.'.join(map(str, self.version[0:2])) + else: + vstring = '.'.join(map(str, self.version)) + + if self.prerelease: + vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) + + return vstring + + def _cmp(self, other): + if isinstance(other, str): + other = StrictVersion(other) + elif not isinstance(other, StrictVersion): + return NotImplemented + + if self.version != other.version: + # numeric versions don't match + # prerelease stuff doesn't matter + if self.version < other.version: + return -1 + else: + return 1 + + # have to compare prerelease + # case 1: neither has prerelease; they're equal + # case 2: self has prerelease, other doesn't; other is greater + # case 3: self doesn't have prerelease, other does: self is greater + # case 4: both have prerelease: must compare them! + + if (not self.prerelease and not other.prerelease): + return 0 + elif (self.prerelease and not other.prerelease): + return -1 + elif (not self.prerelease and other.prerelease): + return 1 + elif (self.prerelease and other.prerelease): + if self.prerelease == other.prerelease: + return 0 + elif self.prerelease < other.prerelease: + return -1 + else: + return 1 + else: + raise AssertionError("never get here") + +# end class StrictVersion + +# The rules according to Greg Stein: +# 1) a version number has 1 or more numbers separated by a period or by +# sequences of letters. If only periods, then these are compared +# left-to-right to determine an ordering. +# 2) sequences of letters are part of the tuple for comparison and are +# compared lexicographically +# 3) recognize the numeric components may have leading zeroes +# +# The LooseVersion class below implements these rules: a version number +# string is split up into a tuple of integer and string components, and +# comparison is a simple tuple comparison. This means that version +# numbers behave in a predictable and obvious way, but a way that might +# not necessarily be how people *want* version numbers to behave. There +# wouldn't be a problem if people could stick to purely numeric version +# numbers: just split on period and compare the numbers as tuples. +# However, people insist on putting letters into their version numbers; +# the most common purpose seems to be: +# - indicating a "pre-release" version +# ('alpha', 'beta', 'a', 'b', 'pre', 'p') +# - indicating a post-release patch ('p', 'pl', 'patch') +# but of course this can't cover all version number schemes, and there's +# no way to know what a programmer means without asking him. +# +# The problem is what to do with letters (and other non-numeric +# characters) in a version number. The current implementation does the +# obvious and predictable thing: keep them as strings and compare +# lexically within a tuple comparison. This has the desired effect if +# an appended letter sequence implies something "post-release": +# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". +# +# However, if letters in a version number imply a pre-release version, +# the "obvious" thing isn't correct. Eg. you would expect that +# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison +# implemented here, this just isn't so. +# +# Two possible solutions come to mind. The first is to tie the +# comparison algorithm to a particular set of semantic rules, as has +# been done in the StrictVersion class above. This works great as long +# as everyone can go along with bondage and discipline. Hopefully a +# (large) subset of Python module programmers will agree that the +# particular flavour of bondage and discipline provided by StrictVersion +# provides enough benefit to be worth using, and will submit their +# version numbering scheme to its domination. The free-thinking +# anarchists in the lot will never give in, though, and something needs +# to be done to accommodate them. +# +# Perhaps a "moderately strict" version class could be implemented that +# lets almost anything slide (syntactically), and makes some heuristic +# assumptions about non-digits in version number strings. This could +# sink into special-case-hell, though; if I was as talented and +# idiosyncratic as Larry Wall, I'd go ahead and implement a class that +# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is +# just as happy dealing with things like "2g6" and "1.13++". I don't +# think I'm smart enough to do it right though. +# +# In any case, I've coded the test suite for this module (see +# ../test/test_version.py) specifically to fail on things like comparing +# "1.2a2" and "1.2". That's not because the *code* is doing anything +# wrong, it's because the simple, obvious design doesn't match my +# complicated, hairy expectations for real-world version numbers. It +# would be a snap to fix the test suite to say, "Yep, LooseVersion does +# the Right Thing" (ie. the code matches the conception). But I'd rather +# have a conception that matches common notions about version numbers. + + +class LooseVersion(Version): + """Version numbering for anarchists and software realists. + Implements the standard interface for version number classes as + described above. A version number consists of a series of numbers, + separated by either periods or strings of letters. When comparing + version numbers, the numeric components will be compared + numerically, and the alphabetic components lexically. The following + are all valid version numbers, in no particular order: + + 1.5.1 + 1.5.2b2 + 161 + 3.10a + 8.02 + 3.4j + 1996.07.12 + 3.2.pl0 + 3.1.1.6 + 2g6 + 11g + 0.960923 + 2.2beta29 + 1.13++ + 5.5.kw + 2.0b1pl0 + + In fact, there is no such thing as an invalid version number under + this scheme; the rules for comparison are simple and predictable, + but may not always give the results you want (for some definition + of "want"). + """ + + component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def parse(self, vstring): + # I've given up on thinking I can reconstruct the version string + # from the parsed tuple -- so I just store the string here for + # use by __str__ + self.vstring = vstring + components = [x for x in self.component_re.split(vstring) if x and x != '.'] + for i, obj in enumerate(components): + try: + components[i] = int(obj) + except ValueError: + pass + + self.version = components + + def __str__(self): + return self.vstring + + def __repr__(self): + return "LooseVersion ('%s')" % str(self) + + def _cmp(self, other): + if isinstance(other, str): + other = LooseVersion(other) + elif not isinstance(other, LooseVersion): + return NotImplemented + + if self.version == other.version: + return 0 + if self.version < other.version: + return -1 + if self.version > other.version: + return 1 + +# end class LooseVersion diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/account.py b/ansible_collections/community/crypto/plugins/module_utils/acme/account.py new file mode 100644 index 000000000..de5eb171d --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/acme/account.py @@ -0,0 +1,252 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# Copyright (c) 2021 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + ACMEProtocolException, + ModuleFailException, +) + + +class ACMEAccount(object): + ''' + ACME account object. Allows to create new accounts, check for existence of accounts, + retrieve account data. + ''' + + def __init__(self, client): + # Set to true to enable logging of all signed requests + self._debug = False + + self.client = client + + def _new_reg(self, contact=None, agreement=None, terms_agreed=False, allow_creation=True, + external_account_binding=None): + ''' + Registers a new ACME account. Returns a pair ``(created, data)``. + Here, ``created`` is ``True`` if the account was created and + ``False`` if it already existed (e.g. it was not newly created), + or does not exist. In case the account was created or exists, + ``data`` contains the account data; otherwise, it is ``None``. + + If specified, ``external_account_binding`` should be a dictionary + with keys ``kid``, ``alg`` and ``key`` + (https://tools.ietf.org/html/rfc8555#section-7.3.4). + + https://tools.ietf.org/html/rfc8555#section-7.3 + ''' + contact = contact or [] + + if self.client.version == 1: + new_reg = { + 'resource': 'new-reg', + 'contact': contact + } + if agreement: + new_reg['agreement'] = agreement + else: + new_reg['agreement'] = self.client.directory['meta']['terms-of-service'] + if external_account_binding is not None: + raise ModuleFailException('External account binding is not supported for ACME v1') + url = self.client.directory['new-reg'] + else: + if (external_account_binding is not None or self.client.directory['meta'].get('externalAccountRequired')) and allow_creation: + # Some ACME servers such as ZeroSSL do not like it when you try to register an existing account + # and provide external_account_binding credentials. Thus we first send a request with allow_creation=False + # to see whether the account already exists. + + # Note that we pass contact here: ZeroSSL does not accept registration calls without contacts, even + # if onlyReturnExisting is set to true. + created, data = self._new_reg(contact=contact, allow_creation=False) + if data: + # An account already exists! Return data + return created, data + # An account does not yet exist. Try to create one next. + + new_reg = { + 'contact': contact + } + if not allow_creation: + # https://tools.ietf.org/html/rfc8555#section-7.3.1 + new_reg['onlyReturnExisting'] = True + if terms_agreed: + new_reg['termsOfServiceAgreed'] = True + url = self.client.directory['newAccount'] + if external_account_binding is not None: + new_reg['externalAccountBinding'] = self.client.sign_request( + { + 'alg': external_account_binding['alg'], + 'kid': external_account_binding['kid'], + 'url': url, + }, + self.client.account_jwk, + self.client.backend.create_mac_key(external_account_binding['alg'], external_account_binding['key']) + ) + elif self.client.directory['meta'].get('externalAccountRequired') and allow_creation: + raise ModuleFailException( + 'To create an account, an external account binding must be specified. ' + 'Use the acme_account module with the external_account_binding option.' + ) + + result, info = self.client.send_signed_request(url, new_reg, fail_on_error=False) + + if info['status'] in ([200, 201] if self.client.version == 1 else [201]): + # Account did not exist + if 'location' in info: + self.client.set_account_uri(info['location']) + return True, result + elif info['status'] == (409 if self.client.version == 1 else 200): + # Account did exist + if result.get('status') == 'deactivated': + # A bug in Pebble (https://github.com/letsencrypt/pebble/issues/179) and + # Boulder (https://github.com/letsencrypt/boulder/issues/3971): this should + # not return a valid account object according to + # https://tools.ietf.org/html/rfc8555#section-7.3.6: + # "Once an account is deactivated, the server MUST NOT accept further + # requests authorized by that account's key." + if not allow_creation: + return False, None + else: + raise ModuleFailException("Account is deactivated") + if 'location' in info: + self.client.set_account_uri(info['location']) + return False, result + elif info['status'] == 400 and result['type'] == 'urn:ietf:params:acme:error:accountDoesNotExist' and not allow_creation: + # Account does not exist (and we did not try to create it) + return False, None + elif info['status'] == 403 and result['type'] == 'urn:ietf:params:acme:error:unauthorized' and 'deactivated' in (result.get('detail') or ''): + # Account has been deactivated; currently works for Pebble; has not been + # implemented for Boulder (https://github.com/letsencrypt/boulder/issues/3971), + # might need adjustment in error detection. + if not allow_creation: + return False, None + else: + raise ModuleFailException("Account is deactivated") + else: + raise ACMEProtocolException( + self.client.module, msg='Registering ACME account failed', info=info, content_json=result) + + def get_account_data(self): + ''' + Retrieve account information. Can only be called when the account + URI is already known (such as after calling setup_account). + Return None if the account was deactivated, or a dict otherwise. + ''' + if self.client.account_uri is None: + raise ModuleFailException("Account URI unknown") + if self.client.version == 1: + data = {} + data['resource'] = 'reg' + result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False) + else: + # try POST-as-GET first (draft-15 or newer) + data = None + result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False) + # check whether that failed with a malformed request error + if info['status'] >= 400 and result.get('type') == 'urn:ietf:params:acme:error:malformed': + # retry as a regular POST (with no changed data) for pre-draft-15 ACME servers + data = {} + result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False) + if info['status'] in (400, 403) and result.get('type') == 'urn:ietf:params:acme:error:unauthorized': + # Returned when account is deactivated + return None + if info['status'] in (400, 404) and result.get('type') == 'urn:ietf:params:acme:error:accountDoesNotExist': + # Returned when account does not exist + return None + if info['status'] < 200 or info['status'] >= 300: + raise ACMEProtocolException( + self.client.module, msg='Error retrieving account data', info=info, content_json=result) + return result + + def setup_account(self, contact=None, agreement=None, terms_agreed=False, + allow_creation=True, remove_account_uri_if_not_exists=False, + external_account_binding=None): + ''' + Detect or create an account on the ACME server. For ACME v1, + as the only way (without knowing an account URI) to test if an + account exists is to try and create one with the provided account + key, this method will always result in an account being present + (except on error situations). For ACME v2, a new account will + only be created if ``allow_creation`` is set to True. + + For ACME v2, ``check_mode`` is fully respected. For ACME v1, the + account might be created if it does not yet exist. + + Return a pair ``(created, account_data)``. Here, ``created`` will + be ``True`` in case the account was created or would be created + (check mode). ``account_data`` will be the current account data, + or ``None`` if the account does not exist. + + The account URI will be stored in ``client.account_uri``; if it is ``None``, + the account does not exist. + + If specified, ``external_account_binding`` should be a dictionary + with keys ``kid``, ``alg`` and ``key`` + (https://tools.ietf.org/html/rfc8555#section-7.3.4). + + https://tools.ietf.org/html/rfc8555#section-7.3 + ''' + + if self.client.account_uri is not None: + created = False + # Verify that the account key belongs to the URI. + # (If update_contact is True, this will be done below.) + account_data = self.get_account_data() + if account_data is None: + if remove_account_uri_if_not_exists and not allow_creation: + self.client.account_uri = None + else: + raise ModuleFailException("Account is deactivated or does not exist!") + else: + created, account_data = self._new_reg( + contact, + agreement=agreement, + terms_agreed=terms_agreed, + allow_creation=allow_creation and not self.client.module.check_mode, + external_account_binding=external_account_binding, + ) + if self.client.module.check_mode and self.client.account_uri is None and allow_creation: + created = True + account_data = { + 'contact': contact or [] + } + return created, account_data + + def update_account(self, account_data, contact=None): + ''' + Update an account on the ACME server. Check mode is fully respected. + + The current account data must be provided as ``account_data``. + + Return a pair ``(updated, account_data)``, where ``updated`` is + ``True`` in case something changed (contact info updated) or + would be changed (check mode), and ``account_data`` the updated + account data. + + https://tools.ietf.org/html/rfc8555#section-7.3.2 + ''' + # Create request + update_request = {} + if contact is not None and account_data.get('contact', []) != contact: + update_request['contact'] = list(contact) + + # No change? + if not update_request: + return False, dict(account_data) + + # Apply change + if self.client.module.check_mode: + account_data = dict(account_data) + account_data.update(update_request) + else: + if self.client.version == 1: + update_request['resource'] = 'reg' + account_data, dummy = self.client.send_signed_request(self.client.account_uri, update_request) + return True, account_data diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/acme.py b/ansible_collections/community/crypto/plugins/module_utils/acme/acme.py new file mode 100644 index 000000000..c054a52f6 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/acme/acme.py @@ -0,0 +1,452 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# Copyright (c) 2021 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import copy +import datetime +import json +import locale +import time +import traceback + +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_bytes +from ansible.module_utils.urls import fetch_url +from ansible.module_utils.six import PY3 + +from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import ( + OpenSSLCLIBackend, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import ( + CryptographyBackend, + CRYPTOGRAPHY_ERROR, + CRYPTOGRAPHY_MINIMAL_VERSION, + CRYPTOGRAPHY_VERSION, + HAS_CURRENT_CRYPTOGRAPHY, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + ACMEProtocolException, + NetworkException, + ModuleFailException, + KeyParsingError, + format_http_status, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( + nopad_b64, +) + +try: + import ipaddress # noqa: F401, pylint: disable=unused-import +except ImportError: + HAS_IPADDRESS = False + IPADDRESS_IMPORT_ERROR = traceback.format_exc() +else: + HAS_IPADDRESS = True + IPADDRESS_IMPORT_ERROR = None + + +RETRY_STATUS_CODES = (408, 429, 503) + + +def _decode_retry(module, response, info, retry_count): + if info['status'] not in RETRY_STATUS_CODES: + return False + + if retry_count >= 5: + raise ACMEProtocolException(module, msg='Giving up after 5 retries', info=info, response=response) + + # 429 and 503 should have a Retry-After header (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) + try: + retry_after = min(max(1, int(info.get('retry-after'))), 60) + except (TypeError, ValueError) as dummy: + retry_after = 10 + module.log('Retrieved a %s HTTP status on %s, retrying in %s seconds' % (format_http_status(info['status']), info['url'], retry_after)) + + time.sleep(retry_after) + return True + + +def _assert_fetch_url_success(module, response, info, allow_redirect=False, allow_client_error=True, allow_server_error=True): + if info['status'] < 0: + raise NetworkException(msg="Failure downloading %s, %s" % (info['url'], info['msg'])) + + if (300 <= info['status'] < 400 and not allow_redirect) or \ + (400 <= info['status'] < 500 and not allow_client_error) or \ + (info['status'] >= 500 and not allow_server_error): + raise ACMEProtocolException(module, info=info, response=response) + + +def _is_failed(info, expected_status_codes=None): + if info['status'] < 200 or info['status'] >= 400: + return True + if expected_status_codes is not None and info['status'] not in expected_status_codes: + return True + return False + + +class ACMEDirectory(object): + ''' + The ACME server directory. Gives access to the available resources, + and allows to obtain a Replay-Nonce. The acme_directory URL + needs to support unauthenticated GET requests; ACME endpoints + requiring authentication are not supported. + https://tools.ietf.org/html/rfc8555#section-7.1.1 + ''' + + def __init__(self, module, account): + self.module = module + self.directory_root = module.params['acme_directory'] + self.version = module.params['acme_version'] + + self.directory, dummy = account.get_request(self.directory_root, get_only=True) + + self.request_timeout = module.params['request_timeout'] + + # Check whether self.version matches what we expect + if self.version == 1: + for key in ('new-reg', 'new-authz', 'new-cert'): + if key not in self.directory: + raise ModuleFailException("ACME directory does not seem to follow protocol ACME v1") + if self.version == 2: + for key in ('newNonce', 'newAccount', 'newOrder'): + if key not in self.directory: + raise ModuleFailException("ACME directory does not seem to follow protocol ACME v2") + # Make sure that 'meta' is always available + if 'meta' not in self.directory: + self.directory['meta'] = {} + + def __getitem__(self, key): + return self.directory[key] + + def get_nonce(self, resource=None): + url = self.directory_root if self.version == 1 else self.directory['newNonce'] + if resource is not None: + url = resource + retry_count = 0 + while True: + response, info = fetch_url(self.module, url, method='HEAD', timeout=self.request_timeout) + if _decode_retry(self.module, response, info, retry_count): + retry_count += 1 + continue + if info['status'] not in (200, 204): + raise NetworkException("Failed to get replay-nonce, got status {0}".format(format_http_status(info['status']))) + if 'replay-nonce' in info: + return info['replay-nonce'] + self.module.log( + 'HEAD to {0} did return status {1}, but no replay-nonce header!'.format(url, format_http_status(info['status']))) + if retry_count >= 5: + raise ACMEProtocolException( + self.module, msg='Was not able to obtain nonce, giving up after 5 retries', info=info, response=response) + retry_count += 1 + + +class ACMEClient(object): + ''' + ACME client object. Handles the authorized communication with the + ACME server. + ''' + + def __init__(self, module, backend): + # Set to true to enable logging of all signed requests + self._debug = False + + self.module = module + self.backend = backend + self.version = module.params['acme_version'] + # account_key path and content are mutually exclusive + self.account_key_file = module.params['account_key_src'] + self.account_key_content = module.params['account_key_content'] + self.account_key_passphrase = module.params['account_key_passphrase'] + + # Grab account URI from module parameters. + # Make sure empty string is treated as None. + self.account_uri = module.params.get('account_uri') or None + + self.request_timeout = module.params['request_timeout'] + + self.account_key_data = None + self.account_jwk = None + self.account_jws_header = None + if self.account_key_file is not None or self.account_key_content is not None: + try: + self.account_key_data = self.parse_key( + key_file=self.account_key_file, + key_content=self.account_key_content, + passphrase=self.account_key_passphrase) + except KeyParsingError as e: + raise ModuleFailException("Error while parsing account key: {msg}".format(msg=e.msg)) + self.account_jwk = self.account_key_data['jwk'] + self.account_jws_header = { + "alg": self.account_key_data['alg'], + "jwk": self.account_jwk, + } + if self.account_uri: + # Make sure self.account_jws_header is updated + self.set_account_uri(self.account_uri) + + self.directory = ACMEDirectory(module, self) + + def set_account_uri(self, uri): + ''' + Set account URI. For ACME v2, it needs to be used to sending signed + requests. + ''' + self.account_uri = uri + if self.version != 1: + self.account_jws_header.pop('jwk') + self.account_jws_header['kid'] = self.account_uri + + def parse_key(self, key_file=None, key_content=None, passphrase=None): + ''' + Parses an RSA or Elliptic Curve key file in PEM format and returns key_data. + In case of an error, raises KeyParsingError. + ''' + if key_file is None and key_content is None: + raise AssertionError('One of key_file and key_content must be specified!') + return self.backend.parse_key(key_file, key_content, passphrase=passphrase) + + def sign_request(self, protected, payload, key_data, encode_payload=True): + ''' + Signs an ACME request. + ''' + try: + if payload is None: + # POST-as-GET + payload64 = '' + else: + # POST + if encode_payload: + payload = self.module.jsonify(payload).encode('utf8') + payload64 = nopad_b64(to_bytes(payload)) + protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8')) + except Exception as e: + raise ModuleFailException("Failed to encode payload / headers as JSON: {0}".format(e)) + + return self.backend.sign(payload64, protected64, key_data) + + def _log(self, msg, data=None): + ''' + Write arguments to acme.log when logging is enabled. + ''' + if self._debug: + with open('acme.log', 'ab') as f: + f.write('[{0}] {1}\n'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%s'), msg).encode('utf-8')) + if data is not None: + f.write('{0}\n\n'.format(json.dumps(data, indent=2, sort_keys=True)).encode('utf-8')) + + def send_signed_request(self, url, payload, key_data=None, jws_header=None, parse_json_result=True, + encode_payload=True, fail_on_error=True, error_msg=None, expected_status_codes=None): + ''' + Sends a JWS signed HTTP POST request to the ACME server and returns + the response as dictionary (if parse_json_result is True) or in raw form + (if parse_json_result is False). + https://tools.ietf.org/html/rfc8555#section-6.2 + + If payload is None, a POST-as-GET is performed. + (https://tools.ietf.org/html/rfc8555#section-6.3) + ''' + key_data = key_data or self.account_key_data + jws_header = jws_header or self.account_jws_header + failed_tries = 0 + while True: + protected = copy.deepcopy(jws_header) + protected["nonce"] = self.directory.get_nonce() + if self.version != 1: + protected["url"] = url + + self._log('URL', url) + self._log('protected', protected) + self._log('payload', payload) + data = self.sign_request(protected, payload, key_data, encode_payload=encode_payload) + if self.version == 1: + data["header"] = jws_header.copy() + for k, v in protected.items(): + dummy = data["header"].pop(k, None) + self._log('signed request', data) + data = self.module.jsonify(data) + + headers = { + 'Content-Type': 'application/jose+json', + } + resp, info = fetch_url(self.module, url, data=data, headers=headers, method='POST', timeout=self.request_timeout) + if _decode_retry(self.module, resp, info, failed_tries): + failed_tries += 1 + continue + _assert_fetch_url_success(self.module, resp, info) + result = {} + + try: + # In Python 2, reading from a closed response yields a TypeError. + # In Python 3, read() simply returns '' + if PY3 and resp.closed: + raise TypeError + content = resp.read() + except (AttributeError, TypeError): + content = info.pop('body', None) + + if content or not parse_json_result: + if (parse_json_result and info['content-type'].startswith('application/json')) or 400 <= info['status'] < 600: + try: + decoded_result = self.module.from_json(content.decode('utf8')) + self._log('parsed result', decoded_result) + # In case of badNonce error, try again (up to 5 times) + # (https://tools.ietf.org/html/rfc8555#section-6.7) + if all(( + 400 <= info['status'] < 600, + decoded_result.get('type') == 'urn:ietf:params:acme:error:badNonce', + failed_tries <= 5, + )): + failed_tries += 1 + continue + if parse_json_result: + result = decoded_result + else: + result = content + except ValueError: + raise NetworkException("Failed to parse the ACME response: {0} {1}".format(url, content)) + else: + result = content + + if fail_on_error and _is_failed(info, expected_status_codes=expected_status_codes): + raise ACMEProtocolException( + self.module, msg=error_msg, info=info, content=content, content_json=result if parse_json_result else None) + return result, info + + def get_request(self, uri, parse_json_result=True, headers=None, get_only=False, + fail_on_error=True, error_msg=None, expected_status_codes=None): + ''' + Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback + to GET if server replies with a status code of 405. + ''' + if not get_only and self.version != 1: + # Try POST-as-GET + content, info = self.send_signed_request(uri, None, parse_json_result=False, fail_on_error=False) + if info['status'] == 405: + # Instead, do unauthenticated GET + get_only = True + else: + # Do unauthenticated GET + get_only = True + + if get_only: + # Perform unauthenticated GET + retry_count = 0 + while True: + resp, info = fetch_url(self.module, uri, method='GET', headers=headers, timeout=self.request_timeout) + if not _decode_retry(self.module, resp, info, retry_count): + break + retry_count += 1 + + _assert_fetch_url_success(self.module, resp, info) + + try: + # In Python 2, reading from a closed response yields a TypeError. + # In Python 3, read() simply returns '' + if PY3 and resp.closed: + raise TypeError + content = resp.read() + except (AttributeError, TypeError): + content = info.pop('body', None) + + # Process result + parsed_json_result = False + if parse_json_result: + result = {} + if content: + if info['content-type'].startswith('application/json'): + try: + result = self.module.from_json(content.decode('utf8')) + parsed_json_result = True + except ValueError: + raise NetworkException("Failed to parse the ACME response: {0} {1}".format(uri, content)) + else: + result = content + else: + result = content + + if fail_on_error and _is_failed(info, expected_status_codes=expected_status_codes): + raise ACMEProtocolException( + self.module, msg=error_msg, info=info, content=content, content_json=result if parsed_json_result else None) + return result, info + + +def get_default_argspec(): + ''' + Provides default argument spec for the options documented in the acme doc fragment. + ''' + return dict( + account_key_src=dict(type='path', aliases=['account_key']), + account_key_content=dict(type='str', no_log=True), + account_key_passphrase=dict(type='str', no_log=True), + account_uri=dict(type='str'), + acme_directory=dict(type='str', required=True), + acme_version=dict(type='int', required=True, choices=[1, 2]), + validate_certs=dict(type='bool', default=True), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']), + request_timeout=dict(type='int', default=10), + ) + + +def create_backend(module, needs_acme_v2): + if not HAS_IPADDRESS: + module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMPORT_ERROR) + + backend = module.params['select_crypto_backend'] + + # Backend autodetect + if backend == 'auto': + backend = 'cryptography' if HAS_CURRENT_CRYPTOGRAPHY else 'openssl' + + # Create backend object + if backend == 'cryptography': + if CRYPTOGRAPHY_ERROR is not None: + # Either we couldn't import cryptography at all, or there was an unexpected error + if CRYPTOGRAPHY_VERSION is None: + msg = missing_required_lib('cryptography') + else: + msg = 'Unexpected error while preparing cryptography: {0}'.format(CRYPTOGRAPHY_ERROR.splitlines()[-1]) + module.fail_json(msg=msg, exception=CRYPTOGRAPHY_ERROR) + if not HAS_CURRENT_CRYPTOGRAPHY: + # We succeeded importing cryptography, but its version is too old. + module.fail_json( + msg='Found cryptography, but only version {0}. {1}'.format( + CRYPTOGRAPHY_VERSION, + missing_required_lib('cryptography >= {0}'.format(CRYPTOGRAPHY_MINIMAL_VERSION)))) + module.debug('Using cryptography backend (library version {0})'.format(CRYPTOGRAPHY_VERSION)) + module_backend = CryptographyBackend(module) + elif backend == 'openssl': + module.debug('Using OpenSSL binary backend') + module_backend = OpenSSLCLIBackend(module) + else: + module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend)) + + # Check common module parameters + if not module.params['validate_certs']: + module.warn( + 'Disabling certificate validation for communications with ACME endpoint. ' + 'This should only be done for testing against a local ACME server for ' + 'development purposes, but *never* for production purposes.' + ) + + if needs_acme_v2 and module.params['acme_version'] < 2: + module.fail_json(msg='The {0} module requires the ACME v2 protocol!'.format(module._name)) + + if module.params['acme_version'] == 1: + module.deprecate("The value 1 for 'acme_version' is deprecated. Please switch to ACME v2", + version='3.0.0', collection_name='community.crypto') + + # AnsibleModule() changes the locale, so change it back to C because we rely + # on datetime.datetime.strptime() when parsing certificate dates. + locale.setlocale(locale.LC_ALL, 'C') + + return module_backend diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/backend_cryptography.py b/ansible_collections/community/crypto/plugins/module_utils/acme/backend_cryptography.py new file mode 100644 index 000000000..207f743f1 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/acme/backend_cryptography.py @@ -0,0 +1,393 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# Copyright (c) 2021 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import base64 +import binascii +import datetime +import os +import sys +import traceback + +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.acme.backends import ( + CryptoBackend, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import ( + ChainMatcher, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + BackendException, + KeyParsingError, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.io import read_file + +from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64 + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + parse_name_field, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_name_to_oid, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + extract_first_pem, +) + +CRYPTOGRAPHY_MINIMAL_VERSION = '1.5' + +CRYPTOGRAPHY_ERROR = None +try: + import cryptography + import cryptography.hazmat.backends + import cryptography.hazmat.primitives.hashes + import cryptography.hazmat.primitives.hmac + import cryptography.hazmat.primitives.asymmetric.ec + import cryptography.hazmat.primitives.asymmetric.padding + import cryptography.hazmat.primitives.asymmetric.rsa + import cryptography.hazmat.primitives.asymmetric.utils + import cryptography.hazmat.primitives.serialization + import cryptography.x509 + import cryptography.x509.oid +except ImportError as dummy: + HAS_CURRENT_CRYPTOGRAPHY = False + CRYPTOGRAPHY_VERSION = None + CRYPTOGRAPHY_ERROR = traceback.format_exc() +else: + CRYPTOGRAPHY_VERSION = cryptography.__version__ + HAS_CURRENT_CRYPTOGRAPHY = (LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion(CRYPTOGRAPHY_MINIMAL_VERSION)) + try: + if HAS_CURRENT_CRYPTOGRAPHY: + _cryptography_backend = cryptography.hazmat.backends.default_backend() + except Exception as dummy: + CRYPTOGRAPHY_ERROR = traceback.format_exc() + + +if sys.version_info[0] >= 3: + # Python 3 (and newer) + def _count_bytes(n): + return (n.bit_length() + 7) // 8 if n > 0 else 0 + + def _convert_int_to_bytes(count, no): + return no.to_bytes(count, byteorder='big') + + def _pad_hex(n, digits): + res = hex(n)[2:] + if len(res) < digits: + res = '0' * (digits - len(res)) + res + return res +else: + # Python 2 + def _count_bytes(n): + if n <= 0: + return 0 + h = '%x' % n + return (len(h) + 1) // 2 + + def _convert_int_to_bytes(count, n): + h = '%x' % n + if len(h) > 2 * count: + raise Exception('Number {1} needs more than {0} bytes!'.format(count, n)) + return ('0' * (2 * count - len(h)) + h).decode('hex') + + def _pad_hex(n, digits): + h = '%x' % n + if len(h) < digits: + h = '0' * (digits - len(h)) + h + return h + + +class CryptographyChainMatcher(ChainMatcher): + @staticmethod + def _parse_key_identifier(key_identifier, name, criterium_idx, module): + if key_identifier: + try: + return binascii.unhexlify(key_identifier.replace(':', '')) + except Exception: + if criterium_idx is None: + module.warn('Criterium has invalid {0} value. Ignoring criterium.'.format(name)) + else: + module.warn('Criterium {0} in select_chain has invalid {1} value. ' + 'Ignoring criterium.'.format(criterium_idx, name)) + return None + + def __init__(self, criterium, module): + self.criterium = criterium + self.test_certificates = criterium.test_certificates + self.subject = [] + self.issuer = [] + if criterium.subject: + self.subject = [ + (cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.subject, 'subject') + ] + if criterium.issuer: + self.issuer = [ + (cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.issuer, 'issuer') + ] + self.subject_key_identifier = CryptographyChainMatcher._parse_key_identifier( + criterium.subject_key_identifier, 'subject_key_identifier', criterium.index, module) + self.authority_key_identifier = CryptographyChainMatcher._parse_key_identifier( + criterium.authority_key_identifier, 'authority_key_identifier', criterium.index, module) + + def _match_subject(self, x509_subject, match_subject): + for oid, value in match_subject: + found = False + for attribute in x509_subject: + if attribute.oid == oid and value == to_native(attribute.value): + found = True + break + if not found: + return False + return True + + def match(self, certificate): + ''' + Check whether an alternate chain matches the specified criterium. + ''' + chain = certificate.chain + if self.test_certificates == 'last': + chain = chain[-1:] + elif self.test_certificates == 'first': + chain = chain[:1] + for cert in chain: + try: + x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography.hazmat.backends.default_backend()) + matches = True + if not self._match_subject(x509.subject, self.subject): + matches = False + if not self._match_subject(x509.issuer, self.issuer): + matches = False + if self.subject_key_identifier: + try: + ext = x509.extensions.get_extension_for_class(cryptography.x509.SubjectKeyIdentifier) + if self.subject_key_identifier != ext.value.digest: + matches = False + except cryptography.x509.ExtensionNotFound: + matches = False + if self.authority_key_identifier: + try: + ext = x509.extensions.get_extension_for_class(cryptography.x509.AuthorityKeyIdentifier) + if self.authority_key_identifier != ext.value.key_identifier: + matches = False + except cryptography.x509.ExtensionNotFound: + matches = False + if matches: + return True + except Exception as e: + self.module.warn('Error while loading certificate {0}: {1}'.format(cert, e)) + return False + + +class CryptographyBackend(CryptoBackend): + def __init__(self, module): + super(CryptographyBackend, self).__init__(module) + + def parse_key(self, key_file=None, key_content=None, passphrase=None): + ''' + Parses an RSA or Elliptic Curve key file in PEM format and returns key_data. + Raises KeyParsingError in case of errors. + ''' + # If key_content is not given, read key_file + if key_content is None: + key_content = read_file(key_file) + else: + key_content = to_bytes(key_content) + # Parse key + try: + key = cryptography.hazmat.primitives.serialization.load_pem_private_key( + key_content, + password=to_bytes(passphrase) if passphrase is not None else None, + backend=_cryptography_backend) + except Exception as e: + raise KeyParsingError('error while loading key: {0}'.format(e)) + if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + pk = key.public_key().public_numbers() + return { + 'key_obj': key, + 'type': 'rsa', + 'alg': 'RS256', + 'jwk': { + "kty": "RSA", + "e": nopad_b64(_convert_int_to_bytes(_count_bytes(pk.e), pk.e)), + "n": nopad_b64(_convert_int_to_bytes(_count_bytes(pk.n), pk.n)), + }, + 'hash': 'sha256', + } + elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): + pk = key.public_key().public_numbers() + if pk.curve.name == 'secp256r1': + bits = 256 + alg = 'ES256' + hashalg = 'sha256' + point_size = 32 + curve = 'P-256' + elif pk.curve.name == 'secp384r1': + bits = 384 + alg = 'ES384' + hashalg = 'sha384' + point_size = 48 + curve = 'P-384' + elif pk.curve.name == 'secp521r1': + # Not yet supported on Let's Encrypt side, see + # https://github.com/letsencrypt/boulder/issues/2217 + bits = 521 + alg = 'ES512' + hashalg = 'sha512' + point_size = 66 + curve = 'P-521' + else: + raise KeyParsingError('unknown elliptic curve: {0}'.format(pk.curve.name)) + num_bytes = (bits + 7) // 8 + return { + 'key_obj': key, + 'type': 'ec', + 'alg': alg, + 'jwk': { + "kty": "EC", + "crv": curve, + "x": nopad_b64(_convert_int_to_bytes(num_bytes, pk.x)), + "y": nopad_b64(_convert_int_to_bytes(num_bytes, pk.y)), + }, + 'hash': hashalg, + 'point_size': point_size, + } + else: + raise KeyParsingError('unknown key type "{0}"'.format(type(key))) + + def sign(self, payload64, protected64, key_data): + sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8') + if 'mac_obj' in key_data: + mac = key_data['mac_obj']() + mac.update(sign_payload) + signature = mac.finalize() + elif isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15() + hashalg = cryptography.hazmat.primitives.hashes.SHA256 + signature = key_data['key_obj'].sign(sign_payload, padding, hashalg()) + elif isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): + if key_data['hash'] == 'sha256': + hashalg = cryptography.hazmat.primitives.hashes.SHA256 + elif key_data['hash'] == 'sha384': + hashalg = cryptography.hazmat.primitives.hashes.SHA384 + elif key_data['hash'] == 'sha512': + hashalg = cryptography.hazmat.primitives.hashes.SHA512 + ecdsa = cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hashalg()) + r, s = cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature(key_data['key_obj'].sign(sign_payload, ecdsa)) + rr = _pad_hex(r, 2 * key_data['point_size']) + ss = _pad_hex(s, 2 * key_data['point_size']) + signature = binascii.unhexlify(rr) + binascii.unhexlify(ss) + + return { + "protected": protected64, + "payload": payload64, + "signature": nopad_b64(signature), + } + + def create_mac_key(self, alg, key): + '''Create a MAC key.''' + if alg == 'HS256': + hashalg = cryptography.hazmat.primitives.hashes.SHA256 + hashbytes = 32 + elif alg == 'HS384': + hashalg = cryptography.hazmat.primitives.hashes.SHA384 + hashbytes = 48 + elif alg == 'HS512': + hashalg = cryptography.hazmat.primitives.hashes.SHA512 + hashbytes = 64 + else: + raise BackendException('Unsupported MAC key algorithm for cryptography backend: {0}'.format(alg)) + key_bytes = base64.urlsafe_b64decode(key) + if len(key_bytes) < hashbytes: + raise BackendException( + '{0} key must be at least {1} bytes long (after Base64 decoding)'.format(alg, hashbytes)) + return { + 'mac_obj': lambda: cryptography.hazmat.primitives.hmac.HMAC( + key_bytes, + hashalg(), + _cryptography_backend), + 'type': 'hmac', + 'alg': alg, + 'jwk': { + 'kty': 'oct', + 'k': key, + }, + } + + def get_csr_identifiers(self, csr_filename=None, csr_content=None): + ''' + Return a set of requested identifiers (CN and SANs) for the CSR. + Each identifier is a pair (type, identifier), where type is either + 'dns' or 'ip'. + ''' + identifiers = set([]) + if csr_content is None: + csr_content = read_file(csr_filename) + else: + csr_content = to_bytes(csr_content) + csr = cryptography.x509.load_pem_x509_csr(csr_content, _cryptography_backend) + for sub in csr.subject: + if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME: + identifiers.add(('dns', sub.value)) + for extension in csr.extensions: + if extension.oid == cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME: + for name in extension.value: + if isinstance(name, cryptography.x509.DNSName): + identifiers.add(('dns', name.value)) + elif isinstance(name, cryptography.x509.IPAddress): + identifiers.add(('ip', name.value.compressed)) + else: + raise BackendException('Found unsupported SAN identifier {0}'.format(name)) + return identifiers + + def get_cert_days(self, cert_filename=None, cert_content=None, now=None): + ''' + Return the days the certificate in cert_filename remains valid and -1 + if the file was not found. If cert_filename contains more than one + certificate, only the first one will be considered. + + If now is not specified, datetime.datetime.now() is used. + ''' + if cert_filename is not None: + cert_content = None + if os.path.exists(cert_filename): + cert_content = read_file(cert_filename) + else: + cert_content = to_bytes(cert_content) + + if cert_content is None: + return -1 + + # Make sure we have at most one PEM. Otherwise cryptography 36.0.0 will barf. + cert_content = to_bytes(extract_first_pem(to_text(cert_content)) or '') + + try: + cert = cryptography.x509.load_pem_x509_certificate(cert_content, _cryptography_backend) + except Exception as e: + if cert_filename is None: + raise BackendException('Cannot parse certificate: {0}'.format(e)) + raise BackendException('Cannot parse certificate {0}: {1}'.format(cert_filename, e)) + + if now is None: + now = datetime.datetime.now() + return (cert.not_valid_after - now).days + + def create_chain_matcher(self, criterium): + ''' + Given a Criterium object, creates a ChainMatcher object. + ''' + return CryptographyChainMatcher(criterium, self.module) diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/backend_openssl_cli.py b/ansible_collections/community/crypto/plugins/module_utils/acme/backend_openssl_cli.py new file mode 100644 index 000000000..dabcbdb3b --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/acme/backend_openssl_cli.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# Copyright (c) 2021 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import base64 +import binascii +import datetime +import os +import re +import tempfile +import traceback + +from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.acme.backends import ( + CryptoBackend, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + BackendException, + KeyParsingError, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64 + +try: + import ipaddress +except ImportError: + pass + + +_OPENSSL_ENVIRONMENT_UPDATE = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + + +class OpenSSLCLIBackend(CryptoBackend): + def __init__(self, module, openssl_binary=None): + super(OpenSSLCLIBackend, self).__init__(module) + if openssl_binary is None: + openssl_binary = module.get_bin_path('openssl', True) + self.openssl_binary = openssl_binary + + def parse_key(self, key_file=None, key_content=None, passphrase=None): + ''' + Parses an RSA or Elliptic Curve key file in PEM format and returns key_data. + Raises KeyParsingError in case of errors. + ''' + if passphrase is not None: + raise KeyParsingError('openssl backend does not support key passphrases') + # If key_file is not given, but key_content, write that to a temporary file + if key_file is None: + fd, tmpsrc = tempfile.mkstemp() + self.module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit + f = os.fdopen(fd, 'wb') + try: + f.write(key_content.encode('utf-8')) + key_file = tmpsrc + except Exception as err: + try: + f.close() + except Exception as dummy: + pass + raise KeyParsingError("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc()) + f.close() + # Parse key + account_key_type = None + with open(key_file, "rt") as f: + for line in f: + m = re.match(r"^\s*-{5,}BEGIN\s+(EC|RSA)\s+PRIVATE\s+KEY-{5,}\s*$", line) + if m is not None: + account_key_type = m.group(1).lower() + break + if account_key_type is None: + # This happens for example if openssl_privatekey created this key + # (as opposed to the OpenSSL binary). For now, we assume this is + # an RSA key. + # FIXME: add some kind of auto-detection + account_key_type = "rsa" + if account_key_type not in ("rsa", "ec"): + raise KeyParsingError('unknown key type "%s"' % account_key_type) + + openssl_keydump_cmd = [self.openssl_binary, account_key_type, "-in", key_file, "-noout", "-text"] + dummy, out, dummy = self.module.run_command( + openssl_keydump_cmd, check_rc=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + + if account_key_type == 'rsa': + pub_hex, pub_exp = re.search( + r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)", + to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL).groups() + pub_exp = "{0:x}".format(int(pub_exp)) + if len(pub_exp) % 2: + pub_exp = "0{0}".format(pub_exp) + + return { + 'key_file': key_file, + 'type': 'rsa', + 'alg': 'RS256', + 'jwk': { + "kty": "RSA", + "e": nopad_b64(binascii.unhexlify(pub_exp.encode("utf-8"))), + "n": nopad_b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))), + }, + 'hash': 'sha256', + } + elif account_key_type == 'ec': + pub_data = re.search( + r"pub:\s*\n\s+04:([a-f0-9\:\s]+?)\nASN1 OID: (\S+)(?:\nNIST CURVE: (\S+))?", + to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL) + if pub_data is None: + raise KeyParsingError('cannot parse elliptic curve key') + pub_hex = binascii.unhexlify(re.sub(r"(\s|:)", "", pub_data.group(1)).encode("utf-8")) + asn1_oid_curve = pub_data.group(2).lower() + nist_curve = pub_data.group(3).lower() if pub_data.group(3) else None + if asn1_oid_curve == 'prime256v1' or nist_curve == 'p-256': + bits = 256 + alg = 'ES256' + hashalg = 'sha256' + point_size = 32 + curve = 'P-256' + elif asn1_oid_curve == 'secp384r1' or nist_curve == 'p-384': + bits = 384 + alg = 'ES384' + hashalg = 'sha384' + point_size = 48 + curve = 'P-384' + elif asn1_oid_curve == 'secp521r1' or nist_curve == 'p-521': + # Not yet supported on Let's Encrypt side, see + # https://github.com/letsencrypt/boulder/issues/2217 + bits = 521 + alg = 'ES512' + hashalg = 'sha512' + point_size = 66 + curve = 'P-521' + else: + raise KeyParsingError('unknown elliptic curve: %s / %s' % (asn1_oid_curve, nist_curve)) + num_bytes = (bits + 7) // 8 + if len(pub_hex) != 2 * num_bytes: + raise KeyParsingError('bad elliptic curve point (%s / %s)' % (asn1_oid_curve, nist_curve)) + return { + 'key_file': key_file, + 'type': 'ec', + 'alg': alg, + 'jwk': { + "kty": "EC", + "crv": curve, + "x": nopad_b64(pub_hex[:num_bytes]), + "y": nopad_b64(pub_hex[num_bytes:]), + }, + 'hash': hashalg, + 'point_size': point_size, + } + + def sign(self, payload64, protected64, key_data): + sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8') + if key_data['type'] == 'hmac': + hex_key = to_native(binascii.hexlify(base64.urlsafe_b64decode(key_data['jwk']['k']))) + cmd_postfix = ["-mac", "hmac", "-macopt", "hexkey:{0}".format(hex_key), "-binary"] + else: + cmd_postfix = ["-sign", key_data['key_file']] + openssl_sign_cmd = [self.openssl_binary, "dgst", "-{0}".format(key_data['hash'])] + cmd_postfix + + dummy, out, dummy = self.module.run_command( + openssl_sign_cmd, data=sign_payload, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + + if key_data['type'] == 'ec': + dummy, der_out, dummy = self.module.run_command( + [self.openssl_binary, "asn1parse", "-inform", "DER"], + data=out, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + expected_len = 2 * key_data['point_size'] + sig = re.findall( + r"prim:\s+INTEGER\s+:([0-9A-F]{1,%s})\n" % expected_len, + to_text(der_out, errors='surrogate_or_strict')) + if len(sig) != 2: + raise BackendException( + "failed to generate Elliptic Curve signature; cannot parse DER output: {0}".format( + to_text(der_out, errors='surrogate_or_strict'))) + sig[0] = (expected_len - len(sig[0])) * '0' + sig[0] + sig[1] = (expected_len - len(sig[1])) * '0' + sig[1] + out = binascii.unhexlify(sig[0]) + binascii.unhexlify(sig[1]) + + return { + "protected": protected64, + "payload": payload64, + "signature": nopad_b64(to_bytes(out)), + } + + def create_mac_key(self, alg, key): + '''Create a MAC key.''' + if alg == 'HS256': + hashalg = 'sha256' + hashbytes = 32 + elif alg == 'HS384': + hashalg = 'sha384' + hashbytes = 48 + elif alg == 'HS512': + hashalg = 'sha512' + hashbytes = 64 + else: + raise BackendException('Unsupported MAC key algorithm for OpenSSL backend: {0}'.format(alg)) + key_bytes = base64.urlsafe_b64decode(key) + if len(key_bytes) < hashbytes: + raise BackendException( + '{0} key must be at least {1} bytes long (after Base64 decoding)'.format(alg, hashbytes)) + return { + 'type': 'hmac', + 'alg': alg, + 'jwk': { + 'kty': 'oct', + 'k': key, + }, + 'hash': hashalg, + } + + @staticmethod + def _normalize_ip(ip): + try: + return to_native(ipaddress.ip_address(to_text(ip)).compressed) + except ValueError: + # We do not want to error out on something IPAddress() cannot parse + return ip + + def get_csr_identifiers(self, csr_filename=None, csr_content=None): + ''' + Return a set of requested identifiers (CN and SANs) for the CSR. + Each identifier is a pair (type, identifier), where type is either + 'dns' or 'ip'. + ''' + filename = csr_filename + data = None + if csr_content is not None: + filename = '/dev/stdin' + data = csr_content.encode('utf-8') + + openssl_csr_cmd = [self.openssl_binary, "req", "-in", filename, "-noout", "-text"] + dummy, out, dummy = self.module.run_command( + openssl_csr_cmd, data=data, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + + identifiers = set([]) + common_name = re.search(r"Subject:.* CN\s?=\s?([^\s,;/]+)", to_text(out, errors='surrogate_or_strict')) + if common_name is not None: + identifiers.add(('dns', common_name.group(1))) + subject_alt_names = re.search( + r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n", + to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL) + if subject_alt_names is not None: + for san in subject_alt_names.group(1).split(", "): + if san.lower().startswith("dns:"): + identifiers.add(('dns', san[4:])) + elif san.lower().startswith("ip:"): + identifiers.add(('ip', self._normalize_ip(san[3:]))) + elif san.lower().startswith("ip address:"): + identifiers.add(('ip', self._normalize_ip(san[11:]))) + else: + raise BackendException('Found unsupported SAN identifier "{0}"'.format(san)) + return identifiers + + def get_cert_days(self, cert_filename=None, cert_content=None, now=None): + ''' + Return the days the certificate in cert_filename remains valid and -1 + if the file was not found. If cert_filename contains more than one + certificate, only the first one will be considered. + + If now is not specified, datetime.datetime.now() is used. + ''' + filename = cert_filename + data = None + if cert_content is not None: + filename = '/dev/stdin' + data = cert_content.encode('utf-8') + cert_filename_suffix = '' + elif cert_filename is not None: + if not os.path.exists(cert_filename): + return -1 + cert_filename_suffix = ' in {0}'.format(cert_filename) + else: + return -1 + + openssl_cert_cmd = [self.openssl_binary, "x509", "-in", filename, "-noout", "-text"] + dummy, out, dummy = self.module.run_command( + openssl_cert_cmd, data=data, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) + try: + not_after_str = re.search(r"\s+Not After\s*:\s+(.*)", to_text(out, errors='surrogate_or_strict')).group(1) + not_after = datetime.datetime.strptime(not_after_str, '%b %d %H:%M:%S %Y %Z') + except AttributeError: + raise BackendException("No 'Not after' date found{0}".format(cert_filename_suffix)) + except ValueError: + raise BackendException("Failed to parse 'Not after' date{0}".format(cert_filename_suffix)) + if now is None: + now = datetime.datetime.now() + return (not_after - now).days + + def create_chain_matcher(self, criterium): + ''' + Given a Criterium object, creates a ChainMatcher object. + ''' + raise BackendException('Alternate chain matching can only be used with the "cryptography" backend.') diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/backends.py b/ansible_collections/community/crypto/plugins/module_utils/acme/backends.py new file mode 100644 index 000000000..5c48e1a74 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/acme/backends.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# Copyright (c) 2021 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc + +from ansible.module_utils import six + + +@six.add_metaclass(abc.ABCMeta) +class CryptoBackend(object): + def __init__(self, module): + self.module = module + + @abc.abstractmethod + def parse_key(self, key_file=None, key_content=None, passphrase=None): + ''' + Parses an RSA or Elliptic Curve key file in PEM format and returns key_data. + Raises KeyParsingError in case of errors. + ''' + + @abc.abstractmethod + def sign(self, payload64, protected64, key_data): + pass + + @abc.abstractmethod + def create_mac_key(self, alg, key): + '''Create a MAC key.''' + + @abc.abstractmethod + def get_csr_identifiers(self, csr_filename=None, csr_content=None): + ''' + Return a set of requested identifiers (CN and SANs) for the CSR. + Each identifier is a pair (type, identifier), where type is either + 'dns' or 'ip'. + ''' + + @abc.abstractmethod + def get_cert_days(self, cert_filename=None, cert_content=None, now=None): + ''' + Return the days the certificate in cert_filename remains valid and -1 + if the file was not found. If cert_filename contains more than one + certificate, only the first one will be considered. + + If now is not specified, datetime.datetime.now() is used. + ''' + + @abc.abstractmethod + def create_chain_matcher(self, criterium): + ''' + Given a Criterium object, creates a ChainMatcher object. + ''' diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/certificates.py b/ansible_collections/community/crypto/plugins/module_utils/acme/certificates.py new file mode 100644 index 000000000..29e5e185f --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/acme/certificates.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# Copyright (c) 2021 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc + +from ansible.module_utils import six + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + ModuleFailException, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( + der_to_pem, + nopad_b64, + process_links, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + split_pem_list, +) + + +class CertificateChain(object): + ''' + Download and parse the certificate chain. + https://tools.ietf.org/html/rfc8555#section-7.4.2 + ''' + + def __init__(self, url): + self.url = url + self.cert = None + self.chain = [] + self.alternates = [] + + @classmethod + def download(cls, client, url): + content, info = client.get_request(url, parse_json_result=False, headers={'Accept': 'application/pem-certificate-chain'}) + + if not content or not info['content-type'].startswith('application/pem-certificate-chain'): + raise ModuleFailException( + "Cannot download certificate chain from {0}, as content type is not application/pem-certificate-chain: {1} (headers: {2})".format( + url, content, info)) + + result = cls(url) + + # Parse data + certs = split_pem_list(content.decode('utf-8'), keep_inbetween=True) + if certs: + result.cert = certs[0] + result.chain = certs[1:] + + process_links(info, lambda link, relation: result._process_links(client, link, relation)) + + if result.cert is None: + raise ModuleFailException("Failed to parse certificate chain download from {0}: {1} (headers: {2})".format(url, content, info)) + + return result + + def _process_links(self, client, link, relation): + if relation == 'up': + # Process link-up headers if there was no chain in reply + if not self.chain: + chain_result, chain_info = client.get_request(link, parse_json_result=False) + if chain_info['status'] in [200, 201]: + self.chain.append(der_to_pem(chain_result)) + elif relation == 'alternate': + self.alternates.append(link) + + def to_json(self): + cert = self.cert.encode('utf8') + chain = ('\n'.join(self.chain)).encode('utf8') + return { + 'cert': cert, + 'chain': chain, + 'full_chain': cert + chain, + } + + +class Criterium(object): + def __init__(self, criterium, index=None): + self.index = index + self.test_certificates = criterium['test_certificates'] + self.subject = criterium['subject'] + self.issuer = criterium['issuer'] + self.subject_key_identifier = criterium['subject_key_identifier'] + self.authority_key_identifier = criterium['authority_key_identifier'] + + +@six.add_metaclass(abc.ABCMeta) +class ChainMatcher(object): + @abc.abstractmethod + def match(self, certificate): + ''' + Check whether a certificate chain (CertificateChain instance) matches. + ''' + + +def retrieve_acme_v1_certificate(client, csr_der): + ''' + Create a new certificate based on the CSR (ACME v1 protocol). + Return the certificate object as dict + https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5 + ''' + new_cert = { + "resource": "new-cert", + "csr": nopad_b64(csr_der), + } + result, info = client.send_signed_request( + client.directory['new-cert'], new_cert, error_msg='Failed to receive certificate', expected_status_codes=[200, 201]) + cert = CertificateChain(info['location']) + cert.cert = der_to_pem(result) + + def f(link, relation): + if relation == 'up': + chain_result, chain_info = client.get_request(link, parse_json_result=False) + if chain_info['status'] in [200, 201]: + del cert.chain[:] + cert.chain.append(der_to_pem(chain_result)) + + process_links(info, f) + return cert diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/challenges.py b/ansible_collections/community/crypto/plugins/module_utils/acme/challenges.py new file mode 100644 index 000000000..3a87ffec1 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/acme/challenges.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# Copyright (c) 2021 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import base64 +import hashlib +import json +import re +import time + +from ansible.module_utils.common.text.converters import to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( + nopad_b64, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + format_error_problem, + ACMEProtocolException, + ModuleFailException, +) + +try: + import ipaddress +except ImportError: + pass + + +def create_key_authorization(client, token): + ''' + Returns the key authorization for the given token + https://tools.ietf.org/html/rfc8555#section-8.1 + ''' + accountkey_json = json.dumps(client.account_jwk, sort_keys=True, separators=(',', ':')) + thumbprint = nopad_b64(hashlib.sha256(accountkey_json.encode('utf8')).digest()) + return "{0}.{1}".format(token, thumbprint) + + +def combine_identifier(identifier_type, identifier): + return '{type}:{identifier}'.format(type=identifier_type, identifier=identifier) + + +def split_identifier(identifier): + parts = identifier.split(':', 1) + if len(parts) != 2: + raise ModuleFailException( + 'Identifier "{identifier}" is not of the form <type>:<identifier>'.format(identifier=identifier)) + return parts + + +class Challenge(object): + def __init__(self, data, url): + self.data = data + + self.type = data['type'] + self.url = url + self.status = data['status'] + self.token = data.get('token') + + @classmethod + def from_json(cls, client, data, url=None): + return cls(data, url or (data['uri'] if client.version == 1 else data['url'])) + + def call_validate(self, client): + challenge_response = {} + if client.version == 1: + token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token) + key_authorization = create_key_authorization(client, token) + challenge_response['resource'] = 'challenge' + challenge_response['keyAuthorization'] = key_authorization + challenge_response['type'] = self.type + client.send_signed_request( + self.url, + challenge_response, + error_msg='Failed to validate challenge', + expected_status_codes=[200, 202], + ) + + def to_json(self): + return self.data.copy() + + def get_validation_data(self, client, identifier_type, identifier): + token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token) + key_authorization = create_key_authorization(client, token) + + if self.type == 'http-01': + # https://tools.ietf.org/html/rfc8555#section-8.3 + return { + 'resource': '.well-known/acme-challenge/{token}'.format(token=token), + 'resource_value': key_authorization, + } + + if self.type == 'dns-01': + if identifier_type != 'dns': + return None + # https://tools.ietf.org/html/rfc8555#section-8.4 + resource = '_acme-challenge' + value = nopad_b64(hashlib.sha256(to_bytes(key_authorization)).digest()) + record = (resource + identifier[1:]) if identifier.startswith('*.') else '{0}.{1}'.format(resource, identifier) + return { + 'resource': resource, + 'resource_value': value, + 'record': record, + } + + if self.type == 'tls-alpn-01': + # https://www.rfc-editor.org/rfc/rfc8737.html#section-3 + if identifier_type == 'ip': + # IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596) + resource = ipaddress.ip_address(identifier).reverse_pointer + if not resource.endswith('.'): + resource += '.' + else: + resource = identifier + value = base64.b64encode(hashlib.sha256(to_bytes(key_authorization)).digest()) + return { + 'resource': resource, + 'resource_original': combine_identifier(identifier_type, identifier), + 'resource_value': value, + } + + # Unknown challenge type: ignore + return None + + +class Authorization(object): + def _setup(self, client, data): + data['uri'] = self.url + self.data = data + self.challenges = [Challenge.from_json(client, challenge) for challenge in data['challenges']] + if client.version == 1 and 'status' not in data: + # https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2 + # "status (required, string): ... + # If this field is missing, then the default value is "pending"." + self.status = 'pending' + else: + self.status = data['status'] + self.identifier = data['identifier']['value'] + self.identifier_type = data['identifier']['type'] + if data.get('wildcard', False): + self.identifier = '*.{0}'.format(self.identifier) + + def __init__(self, url): + self.url = url + + self.data = None + self.challenges = [] + self.status = None + self.identifier_type = None + self.identifier = None + + @classmethod + def from_json(cls, client, data, url): + result = cls(url) + result._setup(client, data) + return result + + @classmethod + def from_url(cls, client, url): + result = cls(url) + result.refresh(client) + return result + + @classmethod + def create(cls, client, identifier_type, identifier): + ''' + Create a new authorization for the given identifier. + Return the authorization object of the new authorization + https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4 + ''' + new_authz = { + "identifier": { + "type": identifier_type, + "value": identifier, + }, + } + if client.version == 1: + url = client.directory['new-authz'] + new_authz["resource"] = "new-authz" + else: + if 'newAuthz' not in client.directory.directory: + raise ACMEProtocolException(client.module, 'ACME endpoint does not support pre-authorization') + url = client.directory['newAuthz'] + + result, info = client.send_signed_request( + url, new_authz, error_msg='Failed to request challenges', expected_status_codes=[200, 201]) + return cls.from_json(client, result, info['location']) + + @property + def combined_identifier(self): + return combine_identifier(self.identifier_type, self.identifier) + + def to_json(self): + return self.data.copy() + + def refresh(self, client): + result, dummy = client.get_request(self.url) + changed = self.data != result + self._setup(client, result) + return changed + + def get_challenge_data(self, client): + ''' + Returns a dict with the data for all proposed (and supported) challenges + of the given authorization. + ''' + data = {} + for challenge in self.challenges: + validation_data = challenge.get_validation_data(client, self.identifier_type, self.identifier) + if validation_data is not None: + data[challenge.type] = validation_data + return data + + def raise_error(self, error_msg, module=None): + ''' + Aborts with a specific error for a challenge. + ''' + error_details = [] + # multiple challenges could have failed at this point, gather error + # details for all of them before failing + for challenge in self.challenges: + if challenge.status == 'invalid': + msg = 'Challenge {type}'.format(type=challenge.type) + if 'error' in challenge.data: + msg = '{msg}: {problem}'.format( + msg=msg, + problem=format_error_problem(challenge.data['error'], subproblem_prefix='{0}.'.format(challenge.type)), + ) + error_details.append(msg) + raise ACMEProtocolException( + module, + 'Failed to validate challenge for {identifier}: {error}. {details}'.format( + identifier=self.combined_identifier, + error=error_msg, + details='; '.join(error_details), + ), + extras=dict( + identifier=self.combined_identifier, + authorization=self.data, + ), + ) + + def find_challenge(self, challenge_type): + for challenge in self.challenges: + if challenge_type == challenge.type: + return challenge + return None + + def wait_for_validation(self, client, callenge_type): + while True: + self.refresh(client) + if self.status in ['valid', 'invalid', 'revoked']: + break + time.sleep(2) + + if self.status == 'invalid': + self.raise_error('Status is "invalid"', module=client.module) + + return self.status == 'valid' + + def call_validate(self, client, challenge_type, wait=True): + ''' + Validate the authorization provided in the auth dict. Returns True + when the validation was successful and False when it was not. + ''' + challenge = self.find_challenge(challenge_type) + if challenge is None: + raise ModuleFailException('Found no challenge of type "{challenge}" for identifier {identifier}!'.format( + challenge=challenge_type, + identifier=self.combined_identifier, + )) + + challenge.call_validate(client) + + if not wait: + return self.status == 'valid' + return self.wait_for_validation(client, challenge_type) + + def deactivate(self, client): + ''' + Deactivates this authorization. + https://community.letsencrypt.org/t/authorization-deactivation/19860/2 + https://tools.ietf.org/html/rfc8555#section-7.5.2 + ''' + if self.status != 'valid': + return + authz_deactivate = { + 'status': 'deactivated' + } + if client.version == 1: + authz_deactivate['resource'] = 'authz' + result, info = client.send_signed_request(self.url, authz_deactivate, fail_on_error=False) + if 200 <= info['status'] < 300 and result.get('status') == 'deactivated': + self.status = 'deactivated' + return True + return False + + +def wait_for_validation(authzs, client): + ''' + Wait until a list of authz is valid. Fail if at least one of them is invalid or revoked. + ''' + while authzs: + authzs_next = [] + for authz in authzs: + authz.refresh(client) + if authz.status in ['valid', 'invalid', 'revoked']: + if authz.status != 'valid': + authz.raise_error('Status is not "valid"', module=client.module) + else: + authzs_next.append(authz) + if authzs_next: + time.sleep(2) + authzs = authzs_next diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/errors.py b/ansible_collections/community/crypto/plugins/module_utils/acme/errors.py new file mode 100644 index 000000000..208a1ae4f --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/acme/errors.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# Copyright (c) 2021 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.six import binary_type, PY3 +from ansible.module_utils.six.moves.http_client import responses as http_responses + + +def format_http_status(status_code): + expl = http_responses.get(status_code) + if not expl: + return str(status_code) + return '%d %s' % (status_code, expl) + + +def format_error_problem(problem, subproblem_prefix=''): + if 'title' in problem: + msg = 'Error "{title}" ({type})'.format( + type=problem['type'], + title=problem['title'], + ) + else: + msg = 'Error {type}'.format(type=problem['type']) + if 'detail' in problem: + msg += ': "{detail}"'.format(detail=problem['detail']) + subproblems = problem.get('subproblems') + if subproblems is not None: + msg = '{msg} Subproblems:'.format(msg=msg) + for index, problem in enumerate(subproblems): + index_str = '{prefix}{index}'.format(prefix=subproblem_prefix, index=index) + msg = '{msg}\n({index}) {problem}'.format( + msg=msg, + index=index_str, + problem=format_error_problem(problem, subproblem_prefix='{0}.'.format(index_str)), + ) + return msg + + +class ModuleFailException(Exception): + ''' + If raised, module.fail_json() will be called with the given parameters after cleanup. + ''' + def __init__(self, msg, **args): + super(ModuleFailException, self).__init__(self, msg) + self.msg = msg + self.module_fail_args = args + + def do_fail(self, module, **arguments): + module.fail_json(msg=self.msg, other=self.module_fail_args, **arguments) + + +class ACMEProtocolException(ModuleFailException): + def __init__(self, module, msg=None, info=None, response=None, content=None, content_json=None, extras=None): + # Try to get hold of content, if response is given and content is not provided + if content is None and content_json is None and response is not None: + try: + # In Python 2, reading from a closed response yields a TypeError. + # In Python 3, read() simply returns '' + if PY3 and response.closed: + raise TypeError + content = response.read() + except (AttributeError, TypeError): + content = info.pop('body', None) + + # Make sure that content_json is None or a dictionary + if content_json is not None and not isinstance(content_json, dict): + if content is None and isinstance(content_json, binary_type): + content = content_json + content_json = None + + # Try to get hold of JSON decoded content, when content is given and JSON not provided + if content_json is None and content is not None and module is not None: + try: + content_json = module.from_json(to_text(content)) + except Exception as e: + pass + + extras = extras or dict() + + if msg is None: + msg = 'ACME request failed' + add_msg = '' + + if info is not None: + url = info['url'] + code = info['status'] + extras['http_url'] = url + extras['http_status'] = code + if code is not None and code >= 400 and content_json is not None and 'type' in content_json: + if 'status' in content_json and content_json['status'] != code: + code = 'status {problem_code} (HTTP status: {http_code})'.format( + http_code=format_http_status(code), problem_code=content_json['status']) + else: + code = 'status {problem_code}'.format(problem_code=format_http_status(code)) + subproblems = content_json.pop('subproblems', None) + add_msg = ' {problem}.'.format(problem=format_error_problem(content_json)) + extras['problem'] = content_json + extras['subproblems'] = subproblems or [] + if subproblems is not None: + add_msg = '{add_msg} Subproblems:'.format(add_msg=add_msg) + for index, problem in enumerate(subproblems): + add_msg = '{add_msg}\n({index}) {problem}.'.format( + add_msg=add_msg, + index=index, + problem=format_error_problem(problem, subproblem_prefix='{0}.'.format(index)), + ) + else: + code = 'HTTP status {code}'.format(code=format_http_status(code)) + if content_json is not None: + add_msg = ' The JSON error result: {content}'.format(content=content_json) + elif content is not None: + add_msg = ' The raw error result: {content}'.format(content=to_text(content)) + msg = '{msg} for {url} with {code}'.format(msg=msg, url=url, code=format_http_status(code)) + elif content_json is not None: + add_msg = ' The JSON result: {content}'.format(content=content_json) + elif content is not None: + add_msg = ' The raw result: {content}'.format(content=to_text(content)) + + super(ACMEProtocolException, self).__init__( + '{msg}.{add_msg}'.format(msg=msg, add_msg=add_msg), + **extras + ) + self.problem = {} + self.subproblems = [] + for k, v in extras.items(): + setattr(self, k, v) + + +class BackendException(ModuleFailException): + pass + + +class NetworkException(ModuleFailException): + pass + + +class KeyParsingError(ModuleFailException): + pass diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/io.py b/ansible_collections/community/crypto/plugins/module_utils/acme/io.py new file mode 100644 index 000000000..898d5a3dd --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/acme/io.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2013, Romeo Theriault <romeot () hawaii.edu> +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# Copyright (c) 2021 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import os +import shutil +import tempfile +import traceback + +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException + + +def read_file(fn, mode='b'): + try: + with open(fn, 'r' + mode) as f: + return f.read() + except Exception as e: + raise ModuleFailException('Error while reading file "{0}": {1}'.format(fn, e)) + + +# This function was adapted from an earlier version of https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/uri.py +def write_file(module, dest, content): + ''' + Write content to destination file dest, only if the content + has changed. + ''' + changed = False + # create a tempfile + fd, tmpsrc = tempfile.mkstemp(text=False) + f = os.fdopen(fd, 'wb') + try: + f.write(content) + except Exception as err: + try: + f.close() + except Exception as dummy: + pass + os.remove(tmpsrc) + raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc()) + f.close() + checksum_src = None + checksum_dest = None + # raise an error if there is no tmpsrc file + if not os.path.exists(tmpsrc): + try: + os.remove(tmpsrc) + except Exception as dummy: + pass + raise ModuleFailException("Source %s does not exist" % (tmpsrc)) + if not os.access(tmpsrc, os.R_OK): + os.remove(tmpsrc) + raise ModuleFailException("Source %s not readable" % (tmpsrc)) + checksum_src = module.sha1(tmpsrc) + # check if there is no dest file + if os.path.exists(dest): + # raise an error if copy has no permission on dest + if not os.access(dest, os.W_OK): + os.remove(tmpsrc) + raise ModuleFailException("Destination %s not writable" % (dest)) + if not os.access(dest, os.R_OK): + os.remove(tmpsrc) + raise ModuleFailException("Destination %s not readable" % (dest)) + checksum_dest = module.sha1(dest) + else: + dirname = os.path.dirname(dest) or '.' + if not os.access(dirname, os.W_OK): + os.remove(tmpsrc) + raise ModuleFailException("Destination dir %s not writable" % (dirname)) + if checksum_src != checksum_dest: + try: + shutil.copyfile(tmpsrc, dest) + changed = True + except Exception as err: + os.remove(tmpsrc) + raise ModuleFailException("failed to copy %s to %s: %s" % (tmpsrc, dest, to_native(err)), exception=traceback.format_exc()) + os.remove(tmpsrc) + return changed diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/orders.py b/ansible_collections/community/crypto/plugins/module_utils/acme/orders.py new file mode 100644 index 000000000..732b430df --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/acme/orders.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# Copyright (c) 2021 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import time + +from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( + nopad_b64, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + ACMEProtocolException, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import ( + Authorization, +) + + +class Order(object): + def _setup(self, client, data): + self.data = data + + self.status = data['status'] + self.identifiers = [] + for identifier in data['identifiers']: + self.identifiers.append((identifier['type'], identifier['value'])) + self.finalize_uri = data.get('finalize') + self.certificate_uri = data.get('certificate') + self.authorization_uris = data['authorizations'] + self.authorizations = {} + + def __init__(self, url): + self.url = url + + self.data = None + + self.status = None + self.identifiers = [] + self.finalize_uri = None + self.certificate_uri = None + self.authorization_uris = [] + self.authorizations = {} + + @classmethod + def from_json(cls, client, data, url): + result = cls(url) + result._setup(client, data) + return result + + @classmethod + def from_url(cls, client, url): + result = cls(url) + result.refresh(client) + return result + + @classmethod + def create(cls, client, identifiers): + ''' + Start a new certificate order (ACME v2 protocol). + https://tools.ietf.org/html/rfc8555#section-7.4 + ''' + acme_identifiers = [] + for identifier_type, identifier in identifiers: + acme_identifiers.append({ + 'type': identifier_type, + 'value': identifier, + }) + new_order = { + "identifiers": acme_identifiers + } + result, info = client.send_signed_request( + client.directory['newOrder'], new_order, error_msg='Failed to start new order', expected_status_codes=[201]) + return cls.from_json(client, result, info['location']) + + def refresh(self, client): + result, dummy = client.get_request(self.url) + changed = self.data != result + self._setup(client, result) + return changed + + def load_authorizations(self, client): + for auth_uri in self.authorization_uris: + authz = Authorization.from_url(client, auth_uri) + self.authorizations[authz.combined_identifier] = authz + + def wait_for_finalization(self, client): + while True: + self.refresh(client) + if self.status in ['valid', 'invalid', 'pending', 'ready']: + break + time.sleep(2) + + if self.status != 'valid': + raise ACMEProtocolException( + client.module, + 'Failed to wait for order to complete; got status "{status}"'.format(status=self.status), + content_json=self.data) + + def finalize(self, client, csr_der, wait=True): + ''' + Create a new certificate based on the csr. + Return the certificate object as dict + https://tools.ietf.org/html/rfc8555#section-7.4 + ''' + new_cert = { + "csr": nopad_b64(csr_der), + } + result, info = client.send_signed_request( + self.finalize_uri, new_cert, error_msg='Failed to finalizing order', expected_status_codes=[200]) + # It is not clear from the RFC whether the finalize call returns the order object or not. + # Instead of using the result, we call self.refresh(client) below. + + if wait: + self.wait_for_finalization(client) + else: + self.refresh(client) + if self.status not in ['procesing', 'valid', 'invalid']: + raise ACMEProtocolException( + client.module, + 'Failed to finalize order; got status "{status}"'.format(status=self.status), + info=info, + content_json=result) diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/utils.py b/ansible_collections/community/crypto/plugins/module_utils/acme/utils.py new file mode 100644 index 000000000..217b6de47 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/acme/utils.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# Copyright (c) 2021 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import base64 +import re +import textwrap +import traceback + +from ansible.module_utils.common.text.converters import to_native +from ansible.module_utils.six.moves.urllib.parse import unquote + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException + + +def nopad_b64(data): + return base64.urlsafe_b64encode(data).decode('utf8').replace("=", "") + + +def der_to_pem(der_cert): + ''' + Convert the DER format certificate in der_cert to a PEM format certificate and return it. + ''' + return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format( + "\n".join(textwrap.wrap(base64.b64encode(der_cert).decode('utf8'), 64))) + + +def pem_to_der(pem_filename=None, pem_content=None): + ''' + Load PEM file, or use PEM file's content, and convert to DER. + + If PEM contains multiple entities, the first entity will be used. + ''' + certificate_lines = [] + if pem_content is not None: + lines = pem_content.splitlines() + elif pem_filename is not None: + try: + with open(pem_filename, "rt") as f: + lines = list(f) + except Exception as err: + raise ModuleFailException("cannot load PEM file {0}: {1}".format(pem_filename, to_native(err)), exception=traceback.format_exc()) + else: + raise ModuleFailException('One of pem_filename and pem_content must be provided') + header_line_count = 0 + for line in lines: + if line.startswith('-----'): + header_line_count += 1 + if header_line_count == 2: + # If certificate file contains other certs appended + # (like intermediate certificates), ignore these. + break + continue + certificate_lines.append(line.strip()) + return base64.b64decode(''.join(certificate_lines)) + + +def process_links(info, callback): + ''' + Process link header, calls callback for every link header with the URL and relation as options. + ''' + if 'link' in info: + link = info['link'] + for url, relation in re.findall(r'<([^>]+)>;\s*rel="(\w+)"', link): + callback(unquote(url), relation) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/_asn1.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/_asn1.py new file mode 100644 index 000000000..e99b75a5e --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/_asn1.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Jordan Borean <jborean93@gmail.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import re + +from ansible.module_utils.common.text.converters import to_bytes + + +""" +An ASN.1 serialized as a string in the OpenSSL format: + [modifier,]type[:value] + +modifier: + The modifier can be 'IMPLICIT:<tag_number><tag_class>,' or 'EXPLICIT:<tag_number><tag_class>' where IMPLICIT + changes the tag of the universal value to encode and EXPLICIT prefixes its tag to the existing universal value. + The tag_number must be set while the tag_class can be 'U', 'A', 'P', or 'C" for 'Universal', 'Application', + 'Private', or 'Context Specific' with C being the default. + +type: + The underlying ASN.1 type of the value specified. Currently only the following have been implemented: + UTF8: The value must be a UTF-8 encoded string. + +value: + The value to encode, the format of this value depends on the <type> specified. +""" +ASN1_STRING_REGEX = re.compile(r'^((?P<tag_type>IMPLICIT|EXPLICIT):(?P<tag_number>\d+)(?P<tag_class>U|A|P|C)?,)?' + r'(?P<value_type>[\w\d]+):(?P<value>.*)') + + +class TagClass: + universal = 0 + application = 1 + context_specific = 2 + private = 3 + + +# Universal tag numbers that can be encoded. +class TagNumber: + utf8_string = 12 + + +def _pack_octet_integer(value): + """ Packs an integer value into 1 or multiple octets. """ + # NOTE: This is *NOT* the same as packing an ASN.1 INTEGER like value. + octets = bytearray() + + # Continue to shift the number by 7 bits and pack into an octet until the + # value is fully packed. + while value: + octet_value = value & 0b01111111 + + # First round (last octet) must have the MSB set. + if len(octets): + octet_value |= 0b10000000 + + octets.append(octet_value) + value >>= 7 + + # Reverse to ensure the higher order octets are first. + octets.reverse() + return bytes(octets) + + +def serialize_asn1_string_as_der(value): + """ Deserializes an ASN.1 string to a DER encoded byte string. """ + asn1_match = ASN1_STRING_REGEX.match(value) + if not asn1_match: + raise ValueError("The ASN.1 serialized string must be in the format [modifier,]type[:value]") + + tag_type = asn1_match.group('tag_type') + tag_number = asn1_match.group('tag_number') + tag_class = asn1_match.group('tag_class') or 'C' + value_type = asn1_match.group('value_type') + asn1_value = asn1_match.group('value') + + if value_type != 'UTF8': + raise ValueError('The ASN.1 serialized string is not a known type "{0}", only UTF8 types are ' + 'supported'.format(value_type)) + + b_value = to_bytes(asn1_value, encoding='utf-8', errors='surrogate_or_strict') + + # We should only do a universal type tag if not IMPLICITLY tagged or the tag class is not universal. + if not tag_type or (tag_type == 'EXPLICIT' and tag_class != 'U'): + b_value = pack_asn1(TagClass.universal, False, TagNumber.utf8_string, b_value) + + if tag_type: + tag_class = { + 'U': TagClass.universal, + 'A': TagClass.application, + 'P': TagClass.private, + 'C': TagClass.context_specific, + }[tag_class] + + # When adding support for more types this should be looked into further. For now it works with UTF8Strings. + constructed = tag_type == 'EXPLICIT' and tag_class != TagClass.universal + b_value = pack_asn1(tag_class, constructed, int(tag_number), b_value) + + return b_value + + +def pack_asn1(tag_class, constructed, tag_number, b_data): + """Pack the value into an ASN.1 data structure. + + The structure for an ASN.1 element is + + | Identifier Octet(s) | Length Octet(s) | Data Octet(s) | + """ + b_asn1_data = bytearray() + + if tag_class < 0 or tag_class > 3: + raise ValueError("tag_class must be between 0 and 3 not %s" % tag_class) + + # Bit 8 and 7 denotes the class. + identifier_octets = tag_class << 6 + # Bit 6 denotes whether the value is primitive or constructed. + identifier_octets |= ((1 if constructed else 0) << 5) + + # Bits 5-1 contain the tag number, if it cannot be encoded in these 5 bits + # then they are set and another octet(s) is used to denote the tag number. + if tag_number < 31: + identifier_octets |= tag_number + b_asn1_data.append(identifier_octets) + else: + identifier_octets |= 31 + b_asn1_data.append(identifier_octets) + b_asn1_data.extend(_pack_octet_integer(tag_number)) + + length = len(b_data) + + # If the length can be encoded in 7 bits only 1 octet is required. + if length < 128: + b_asn1_data.append(length) + + else: + # Otherwise the length must be encoded across multiple octets + length_octets = bytearray() + while length: + length_octets.append(length & 0b11111111) + length >>= 8 + + length_octets.reverse() # Reverse to make the higher octets first. + + # The first length octet must have the MSB set alongside the number of + # octets the length was encoded in. + b_asn1_data.append(len(length_octets) | 0b10000000) + b_asn1_data.extend(length_octets) + + return bytes(b_asn1_data) + b_data diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/_obj2txt.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/_obj2txt.py new file mode 100644 index 000000000..1ac283673 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/_obj2txt.py @@ -0,0 +1,57 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is licensed under the +# Apache 2.0 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. + +# This excerpt is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file at +# https://github.com/pyca/cryptography/blob/master/LICENSE for complete details. +# +# The Apache 2.0 license has been included as LICENSES/Apache-2.0.txt in this collection. +# The BSD License license has been included as LICENSES/BSD-3-Clause.txt in this collection. +# SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +# +# Adapted from cryptography's hazmat/backends/openssl/decode_asn1.py +# +# Copyright (c) 2015, 2016 Paul Kehrer (@reaperhulk) +# Copyright (c) 2017 Fraser Tweedale (@frasertweedale) + +# Relevant commits from cryptography project (https://github.com/pyca/cryptography): +# pyca/cryptography@719d536dd691e84e208534798f2eb4f82aaa2e07 +# pyca/cryptography@5ab6d6a5c05572bd1c75f05baf264a2d0001894a +# pyca/cryptography@2e776e20eb60378e0af9b7439000d0e80da7c7e3 +# pyca/cryptography@fb309ed24647d1be9e319b61b1f2aa8ebb87b90b +# pyca/cryptography@2917e460993c475c72d7146c50dc3bbc2414280d +# pyca/cryptography@3057f91ea9a05fb593825006d87a391286a4d828 +# pyca/cryptography@d607dd7e5bc5c08854ec0c9baff70ba4a35be36f + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +# WARNING: this function no longer works with cryptography 35.0.0 and newer! +# It must **ONLY** be used in compatibility code for older +# cryptography versions! + +def obj2txt(openssl_lib, openssl_ffi, obj): + # Set to 80 on the recommendation of + # https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values + # + # But OIDs longer than this occur in real life (e.g. Active + # Directory makes some very long OIDs). So we need to detect + # and properly handle the case where the default buffer is not + # big enough. + # + buf_len = 80 + buf = openssl_ffi.new("char[]", buf_len) + + # 'res' is the number of bytes that *would* be written if the + # buffer is large enough. If 'res' > buf_len - 1, we need to + # alloc a big-enough buffer and go again. + res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1) + if res > buf_len - 1: # account for terminating null byte + buf_len = res + 1 + buf = openssl_ffi.new("char[]", buf_len) + res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1) + return openssl_ffi.buffer(buf, res)[:].decode() diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/_objects.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/_objects.py new file mode 100644 index 000000000..ed225805f --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/_objects.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ._objects_data import OID_MAP + +OID_LOOKUP = dict() +NORMALIZE_NAMES = dict() +NORMALIZE_NAMES_SHORT = dict() + +for dotted, names in OID_MAP.items(): + for name in names: + if name in NORMALIZE_NAMES and OID_LOOKUP[name] != dotted: + raise AssertionError( + 'Name collision during setup: "{0}" for OIDs {1} and {2}' + .format(name, dotted, OID_LOOKUP[name]) + ) + NORMALIZE_NAMES[name] = names[0] + NORMALIZE_NAMES_SHORT[name] = names[-1] + OID_LOOKUP[name] = dotted +for alias, original in [('userID', 'userId')]: + if alias in NORMALIZE_NAMES: + raise AssertionError( + 'Name collision during adding aliases: "{0}" (alias for "{1}") is already mapped to OID {2}' + .format(alias, original, OID_LOOKUP[alias]) + ) + NORMALIZE_NAMES[alias] = original + NORMALIZE_NAMES_SHORT[alias] = NORMALIZE_NAMES_SHORT[original] + OID_LOOKUP[alias] = OID_LOOKUP[original] diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/_objects_data.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/_objects_data.py new file mode 100644 index 000000000..4d57b2ef9 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/_objects_data.py @@ -0,0 +1,1115 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is licensed under the +# Apache 2.0 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. + +# This has been extracted from the OpenSSL project's objects.txt: +# https://github.com/openssl/openssl/blob/9537fe5757bb07761fa275d779bbd40bcf5530e4/crypto/objects/objects.txt +# Extracted with https://gist.github.com/felixfontein/376748017ad65ead093d56a45a5bf376 + +# In case the following data structure has any copyrightable content, note that it is licensed as follows: +# Copyright (c) the OpenSSL contributors +# Licensed under the Apache License 2.0 +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/openssl/openssl/blob/master/LICENSE.txt or LICENSES/Apache-2.0.txt + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +OID_MAP = { + '0': ('itu-t', 'ITU-T', 'ccitt'), + '0.3.4401.5': ('ntt-ds', ), + '0.3.4401.5.3.1.9': ('camellia', ), + '0.3.4401.5.3.1.9.1': ('camellia-128-ecb', 'CAMELLIA-128-ECB'), + '0.3.4401.5.3.1.9.3': ('camellia-128-ofb', 'CAMELLIA-128-OFB'), + '0.3.4401.5.3.1.9.4': ('camellia-128-cfb', 'CAMELLIA-128-CFB'), + '0.3.4401.5.3.1.9.6': ('camellia-128-gcm', 'CAMELLIA-128-GCM'), + '0.3.4401.5.3.1.9.7': ('camellia-128-ccm', 'CAMELLIA-128-CCM'), + '0.3.4401.5.3.1.9.9': ('camellia-128-ctr', 'CAMELLIA-128-CTR'), + '0.3.4401.5.3.1.9.10': ('camellia-128-cmac', 'CAMELLIA-128-CMAC'), + '0.3.4401.5.3.1.9.21': ('camellia-192-ecb', 'CAMELLIA-192-ECB'), + '0.3.4401.5.3.1.9.23': ('camellia-192-ofb', 'CAMELLIA-192-OFB'), + '0.3.4401.5.3.1.9.24': ('camellia-192-cfb', 'CAMELLIA-192-CFB'), + '0.3.4401.5.3.1.9.26': ('camellia-192-gcm', 'CAMELLIA-192-GCM'), + '0.3.4401.5.3.1.9.27': ('camellia-192-ccm', 'CAMELLIA-192-CCM'), + '0.3.4401.5.3.1.9.29': ('camellia-192-ctr', 'CAMELLIA-192-CTR'), + '0.3.4401.5.3.1.9.30': ('camellia-192-cmac', 'CAMELLIA-192-CMAC'), + '0.3.4401.5.3.1.9.41': ('camellia-256-ecb', 'CAMELLIA-256-ECB'), + '0.3.4401.5.3.1.9.43': ('camellia-256-ofb', 'CAMELLIA-256-OFB'), + '0.3.4401.5.3.1.9.44': ('camellia-256-cfb', 'CAMELLIA-256-CFB'), + '0.3.4401.5.3.1.9.46': ('camellia-256-gcm', 'CAMELLIA-256-GCM'), + '0.3.4401.5.3.1.9.47': ('camellia-256-ccm', 'CAMELLIA-256-CCM'), + '0.3.4401.5.3.1.9.49': ('camellia-256-ctr', 'CAMELLIA-256-CTR'), + '0.3.4401.5.3.1.9.50': ('camellia-256-cmac', 'CAMELLIA-256-CMAC'), + '0.9': ('data', ), + '0.9.2342': ('pss', ), + '0.9.2342.19200300': ('ucl', ), + '0.9.2342.19200300.100': ('pilot', ), + '0.9.2342.19200300.100.1': ('pilotAttributeType', ), + '0.9.2342.19200300.100.1.1': ('userId', 'UID'), + '0.9.2342.19200300.100.1.2': ('textEncodedORAddress', ), + '0.9.2342.19200300.100.1.3': ('rfc822Mailbox', 'mail'), + '0.9.2342.19200300.100.1.4': ('info', ), + '0.9.2342.19200300.100.1.5': ('favouriteDrink', ), + '0.9.2342.19200300.100.1.6': ('roomNumber', ), + '0.9.2342.19200300.100.1.7': ('photo', ), + '0.9.2342.19200300.100.1.8': ('userClass', ), + '0.9.2342.19200300.100.1.9': ('host', ), + '0.9.2342.19200300.100.1.10': ('manager', ), + '0.9.2342.19200300.100.1.11': ('documentIdentifier', ), + '0.9.2342.19200300.100.1.12': ('documentTitle', ), + '0.9.2342.19200300.100.1.13': ('documentVersion', ), + '0.9.2342.19200300.100.1.14': ('documentAuthor', ), + '0.9.2342.19200300.100.1.15': ('documentLocation', ), + '0.9.2342.19200300.100.1.20': ('homeTelephoneNumber', ), + '0.9.2342.19200300.100.1.21': ('secretary', ), + '0.9.2342.19200300.100.1.22': ('otherMailbox', ), + '0.9.2342.19200300.100.1.23': ('lastModifiedTime', ), + '0.9.2342.19200300.100.1.24': ('lastModifiedBy', ), + '0.9.2342.19200300.100.1.25': ('domainComponent', 'DC'), + '0.9.2342.19200300.100.1.26': ('aRecord', ), + '0.9.2342.19200300.100.1.27': ('pilotAttributeType27', ), + '0.9.2342.19200300.100.1.28': ('mXRecord', ), + '0.9.2342.19200300.100.1.29': ('nSRecord', ), + '0.9.2342.19200300.100.1.30': ('sOARecord', ), + '0.9.2342.19200300.100.1.31': ('cNAMERecord', ), + '0.9.2342.19200300.100.1.37': ('associatedDomain', ), + '0.9.2342.19200300.100.1.38': ('associatedName', ), + '0.9.2342.19200300.100.1.39': ('homePostalAddress', ), + '0.9.2342.19200300.100.1.40': ('personalTitle', ), + '0.9.2342.19200300.100.1.41': ('mobileTelephoneNumber', ), + '0.9.2342.19200300.100.1.42': ('pagerTelephoneNumber', ), + '0.9.2342.19200300.100.1.43': ('friendlyCountryName', ), + '0.9.2342.19200300.100.1.44': ('uniqueIdentifier', 'uid'), + '0.9.2342.19200300.100.1.45': ('organizationalStatus', ), + '0.9.2342.19200300.100.1.46': ('janetMailbox', ), + '0.9.2342.19200300.100.1.47': ('mailPreferenceOption', ), + '0.9.2342.19200300.100.1.48': ('buildingName', ), + '0.9.2342.19200300.100.1.49': ('dSAQuality', ), + '0.9.2342.19200300.100.1.50': ('singleLevelQuality', ), + '0.9.2342.19200300.100.1.51': ('subtreeMinimumQuality', ), + '0.9.2342.19200300.100.1.52': ('subtreeMaximumQuality', ), + '0.9.2342.19200300.100.1.53': ('personalSignature', ), + '0.9.2342.19200300.100.1.54': ('dITRedirect', ), + '0.9.2342.19200300.100.1.55': ('audio', ), + '0.9.2342.19200300.100.1.56': ('documentPublisher', ), + '0.9.2342.19200300.100.3': ('pilotAttributeSyntax', ), + '0.9.2342.19200300.100.3.4': ('iA5StringSyntax', ), + '0.9.2342.19200300.100.3.5': ('caseIgnoreIA5StringSyntax', ), + '0.9.2342.19200300.100.4': ('pilotObjectClass', ), + '0.9.2342.19200300.100.4.3': ('pilotObject', ), + '0.9.2342.19200300.100.4.4': ('pilotPerson', ), + '0.9.2342.19200300.100.4.5': ('account', ), + '0.9.2342.19200300.100.4.6': ('document', ), + '0.9.2342.19200300.100.4.7': ('room', ), + '0.9.2342.19200300.100.4.9': ('documentSeries', ), + '0.9.2342.19200300.100.4.13': ('Domain', 'domain'), + '0.9.2342.19200300.100.4.14': ('rFC822localPart', ), + '0.9.2342.19200300.100.4.15': ('dNSDomain', ), + '0.9.2342.19200300.100.4.17': ('domainRelatedObject', ), + '0.9.2342.19200300.100.4.18': ('friendlyCountry', ), + '0.9.2342.19200300.100.4.19': ('simpleSecurityObject', ), + '0.9.2342.19200300.100.4.20': ('pilotOrganization', ), + '0.9.2342.19200300.100.4.21': ('pilotDSA', ), + '0.9.2342.19200300.100.4.22': ('qualityLabelledData', ), + '0.9.2342.19200300.100.10': ('pilotGroups', ), + '1': ('iso', 'ISO'), + '1.0.9797.3.4': ('gmac', 'GMAC'), + '1.0.10118.3.0.55': ('whirlpool', ), + '1.2': ('ISO Member Body', 'member-body'), + '1.2.156': ('ISO CN Member Body', 'ISO-CN'), + '1.2.156.10197': ('oscca', ), + '1.2.156.10197.1': ('sm-scheme', ), + '1.2.156.10197.1.104.1': ('sm4-ecb', 'SM4-ECB'), + '1.2.156.10197.1.104.2': ('sm4-cbc', 'SM4-CBC'), + '1.2.156.10197.1.104.3': ('sm4-ofb', 'SM4-OFB'), + '1.2.156.10197.1.104.4': ('sm4-cfb', 'SM4-CFB'), + '1.2.156.10197.1.104.5': ('sm4-cfb1', 'SM4-CFB1'), + '1.2.156.10197.1.104.6': ('sm4-cfb8', 'SM4-CFB8'), + '1.2.156.10197.1.104.7': ('sm4-ctr', 'SM4-CTR'), + '1.2.156.10197.1.301': ('sm2', 'SM2'), + '1.2.156.10197.1.401': ('sm3', 'SM3'), + '1.2.156.10197.1.501': ('SM2-with-SM3', 'SM2-SM3'), + '1.2.156.10197.1.504': ('sm3WithRSAEncryption', 'RSA-SM3'), + '1.2.392.200011.61.1.1.1.2': ('camellia-128-cbc', 'CAMELLIA-128-CBC'), + '1.2.392.200011.61.1.1.1.3': ('camellia-192-cbc', 'CAMELLIA-192-CBC'), + '1.2.392.200011.61.1.1.1.4': ('camellia-256-cbc', 'CAMELLIA-256-CBC'), + '1.2.392.200011.61.1.1.3.2': ('id-camellia128-wrap', ), + '1.2.392.200011.61.1.1.3.3': ('id-camellia192-wrap', ), + '1.2.392.200011.61.1.1.3.4': ('id-camellia256-wrap', ), + '1.2.410.200004': ('kisa', 'KISA'), + '1.2.410.200004.1.3': ('seed-ecb', 'SEED-ECB'), + '1.2.410.200004.1.4': ('seed-cbc', 'SEED-CBC'), + '1.2.410.200004.1.5': ('seed-cfb', 'SEED-CFB'), + '1.2.410.200004.1.6': ('seed-ofb', 'SEED-OFB'), + '1.2.410.200046.1.1': ('aria', ), + '1.2.410.200046.1.1.1': ('aria-128-ecb', 'ARIA-128-ECB'), + '1.2.410.200046.1.1.2': ('aria-128-cbc', 'ARIA-128-CBC'), + '1.2.410.200046.1.1.3': ('aria-128-cfb', 'ARIA-128-CFB'), + '1.2.410.200046.1.1.4': ('aria-128-ofb', 'ARIA-128-OFB'), + '1.2.410.200046.1.1.5': ('aria-128-ctr', 'ARIA-128-CTR'), + '1.2.410.200046.1.1.6': ('aria-192-ecb', 'ARIA-192-ECB'), + '1.2.410.200046.1.1.7': ('aria-192-cbc', 'ARIA-192-CBC'), + '1.2.410.200046.1.1.8': ('aria-192-cfb', 'ARIA-192-CFB'), + '1.2.410.200046.1.1.9': ('aria-192-ofb', 'ARIA-192-OFB'), + '1.2.410.200046.1.1.10': ('aria-192-ctr', 'ARIA-192-CTR'), + '1.2.410.200046.1.1.11': ('aria-256-ecb', 'ARIA-256-ECB'), + '1.2.410.200046.1.1.12': ('aria-256-cbc', 'ARIA-256-CBC'), + '1.2.410.200046.1.1.13': ('aria-256-cfb', 'ARIA-256-CFB'), + '1.2.410.200046.1.1.14': ('aria-256-ofb', 'ARIA-256-OFB'), + '1.2.410.200046.1.1.15': ('aria-256-ctr', 'ARIA-256-CTR'), + '1.2.410.200046.1.1.34': ('aria-128-gcm', 'ARIA-128-GCM'), + '1.2.410.200046.1.1.35': ('aria-192-gcm', 'ARIA-192-GCM'), + '1.2.410.200046.1.1.36': ('aria-256-gcm', 'ARIA-256-GCM'), + '1.2.410.200046.1.1.37': ('aria-128-ccm', 'ARIA-128-CCM'), + '1.2.410.200046.1.1.38': ('aria-192-ccm', 'ARIA-192-CCM'), + '1.2.410.200046.1.1.39': ('aria-256-ccm', 'ARIA-256-CCM'), + '1.2.643.2.2': ('cryptopro', ), + '1.2.643.2.2.3': ('GOST R 34.11-94 with GOST R 34.10-2001', 'id-GostR3411-94-with-GostR3410-2001'), + '1.2.643.2.2.4': ('GOST R 34.11-94 with GOST R 34.10-94', 'id-GostR3411-94-with-GostR3410-94'), + '1.2.643.2.2.9': ('GOST R 34.11-94', 'md_gost94'), + '1.2.643.2.2.10': ('HMAC GOST 34.11-94', 'id-HMACGostR3411-94'), + '1.2.643.2.2.14.0': ('id-Gost28147-89-None-KeyMeshing', ), + '1.2.643.2.2.14.1': ('id-Gost28147-89-CryptoPro-KeyMeshing', ), + '1.2.643.2.2.19': ('GOST R 34.10-2001', 'gost2001'), + '1.2.643.2.2.20': ('GOST R 34.10-94', 'gost94'), + '1.2.643.2.2.20.1': ('id-GostR3410-94-a', ), + '1.2.643.2.2.20.2': ('id-GostR3410-94-aBis', ), + '1.2.643.2.2.20.3': ('id-GostR3410-94-b', ), + '1.2.643.2.2.20.4': ('id-GostR3410-94-bBis', ), + '1.2.643.2.2.21': ('GOST 28147-89', 'gost89'), + '1.2.643.2.2.22': ('GOST 28147-89 MAC', 'gost-mac'), + '1.2.643.2.2.23': ('GOST R 34.11-94 PRF', 'prf-gostr3411-94'), + '1.2.643.2.2.30.0': ('id-GostR3411-94-TestParamSet', ), + '1.2.643.2.2.30.1': ('id-GostR3411-94-CryptoProParamSet', ), + '1.2.643.2.2.31.0': ('id-Gost28147-89-TestParamSet', ), + '1.2.643.2.2.31.1': ('id-Gost28147-89-CryptoPro-A-ParamSet', ), + '1.2.643.2.2.31.2': ('id-Gost28147-89-CryptoPro-B-ParamSet', ), + '1.2.643.2.2.31.3': ('id-Gost28147-89-CryptoPro-C-ParamSet', ), + '1.2.643.2.2.31.4': ('id-Gost28147-89-CryptoPro-D-ParamSet', ), + '1.2.643.2.2.31.5': ('id-Gost28147-89-CryptoPro-Oscar-1-1-ParamSet', ), + '1.2.643.2.2.31.6': ('id-Gost28147-89-CryptoPro-Oscar-1-0-ParamSet', ), + '1.2.643.2.2.31.7': ('id-Gost28147-89-CryptoPro-RIC-1-ParamSet', ), + '1.2.643.2.2.32.0': ('id-GostR3410-94-TestParamSet', ), + '1.2.643.2.2.32.2': ('id-GostR3410-94-CryptoPro-A-ParamSet', ), + '1.2.643.2.2.32.3': ('id-GostR3410-94-CryptoPro-B-ParamSet', ), + '1.2.643.2.2.32.4': ('id-GostR3410-94-CryptoPro-C-ParamSet', ), + '1.2.643.2.2.32.5': ('id-GostR3410-94-CryptoPro-D-ParamSet', ), + '1.2.643.2.2.33.1': ('id-GostR3410-94-CryptoPro-XchA-ParamSet', ), + '1.2.643.2.2.33.2': ('id-GostR3410-94-CryptoPro-XchB-ParamSet', ), + '1.2.643.2.2.33.3': ('id-GostR3410-94-CryptoPro-XchC-ParamSet', ), + '1.2.643.2.2.35.0': ('id-GostR3410-2001-TestParamSet', ), + '1.2.643.2.2.35.1': ('id-GostR3410-2001-CryptoPro-A-ParamSet', ), + '1.2.643.2.2.35.2': ('id-GostR3410-2001-CryptoPro-B-ParamSet', ), + '1.2.643.2.2.35.3': ('id-GostR3410-2001-CryptoPro-C-ParamSet', ), + '1.2.643.2.2.36.0': ('id-GostR3410-2001-CryptoPro-XchA-ParamSet', ), + '1.2.643.2.2.36.1': ('id-GostR3410-2001-CryptoPro-XchB-ParamSet', ), + '1.2.643.2.2.98': ('GOST R 34.10-2001 DH', 'id-GostR3410-2001DH'), + '1.2.643.2.2.99': ('GOST R 34.10-94 DH', 'id-GostR3410-94DH'), + '1.2.643.2.9': ('cryptocom', ), + '1.2.643.2.9.1.3.3': ('GOST R 34.11-94 with GOST R 34.10-94 Cryptocom', 'id-GostR3411-94-with-GostR3410-94-cc'), + '1.2.643.2.9.1.3.4': ('GOST R 34.11-94 with GOST R 34.10-2001 Cryptocom', 'id-GostR3411-94-with-GostR3410-2001-cc'), + '1.2.643.2.9.1.5.3': ('GOST 34.10-94 Cryptocom', 'gost94cc'), + '1.2.643.2.9.1.5.4': ('GOST 34.10-2001 Cryptocom', 'gost2001cc'), + '1.2.643.2.9.1.6.1': ('GOST 28147-89 Cryptocom ParamSet', 'id-Gost28147-89-cc'), + '1.2.643.2.9.1.8.1': ('GOST R 3410-2001 Parameter Set Cryptocom', 'id-GostR3410-2001-ParamSet-cc'), + '1.2.643.3.131.1.1': ('INN', 'INN'), + '1.2.643.7.1': ('id-tc26', ), + '1.2.643.7.1.1': ('id-tc26-algorithms', ), + '1.2.643.7.1.1.1': ('id-tc26-sign', ), + '1.2.643.7.1.1.1.1': ('GOST R 34.10-2012 with 256 bit modulus', 'gost2012_256'), + '1.2.643.7.1.1.1.2': ('GOST R 34.10-2012 with 512 bit modulus', 'gost2012_512'), + '1.2.643.7.1.1.2': ('id-tc26-digest', ), + '1.2.643.7.1.1.2.2': ('GOST R 34.11-2012 with 256 bit hash', 'md_gost12_256'), + '1.2.643.7.1.1.2.3': ('GOST R 34.11-2012 with 512 bit hash', 'md_gost12_512'), + '1.2.643.7.1.1.3': ('id-tc26-signwithdigest', ), + '1.2.643.7.1.1.3.2': ('GOST R 34.10-2012 with GOST R 34.11-2012 (256 bit)', 'id-tc26-signwithdigest-gost3410-2012-256'), + '1.2.643.7.1.1.3.3': ('GOST R 34.10-2012 with GOST R 34.11-2012 (512 bit)', 'id-tc26-signwithdigest-gost3410-2012-512'), + '1.2.643.7.1.1.4': ('id-tc26-mac', ), + '1.2.643.7.1.1.4.1': ('HMAC GOST 34.11-2012 256 bit', 'id-tc26-hmac-gost-3411-2012-256'), + '1.2.643.7.1.1.4.2': ('HMAC GOST 34.11-2012 512 bit', 'id-tc26-hmac-gost-3411-2012-512'), + '1.2.643.7.1.1.5': ('id-tc26-cipher', ), + '1.2.643.7.1.1.5.1': ('id-tc26-cipher-gostr3412-2015-magma', ), + '1.2.643.7.1.1.5.1.1': ('id-tc26-cipher-gostr3412-2015-magma-ctracpkm', ), + '1.2.643.7.1.1.5.1.2': ('id-tc26-cipher-gostr3412-2015-magma-ctracpkm-omac', ), + '1.2.643.7.1.1.5.2': ('id-tc26-cipher-gostr3412-2015-kuznyechik', ), + '1.2.643.7.1.1.5.2.1': ('id-tc26-cipher-gostr3412-2015-kuznyechik-ctracpkm', ), + '1.2.643.7.1.1.5.2.2': ('id-tc26-cipher-gostr3412-2015-kuznyechik-ctracpkm-omac', ), + '1.2.643.7.1.1.6': ('id-tc26-agreement', ), + '1.2.643.7.1.1.6.1': ('id-tc26-agreement-gost-3410-2012-256', ), + '1.2.643.7.1.1.6.2': ('id-tc26-agreement-gost-3410-2012-512', ), + '1.2.643.7.1.1.7': ('id-tc26-wrap', ), + '1.2.643.7.1.1.7.1': ('id-tc26-wrap-gostr3412-2015-magma', ), + '1.2.643.7.1.1.7.1.1': ('id-tc26-wrap-gostr3412-2015-magma-kexp15', 'id-tc26-wrap-gostr3412-2015-kuznyechik-kexp15'), + '1.2.643.7.1.1.7.2': ('id-tc26-wrap-gostr3412-2015-kuznyechik', ), + '1.2.643.7.1.2': ('id-tc26-constants', ), + '1.2.643.7.1.2.1': ('id-tc26-sign-constants', ), + '1.2.643.7.1.2.1.1': ('id-tc26-gost-3410-2012-256-constants', ), + '1.2.643.7.1.2.1.1.1': ('GOST R 34.10-2012 (256 bit) ParamSet A', 'id-tc26-gost-3410-2012-256-paramSetA'), + '1.2.643.7.1.2.1.1.2': ('GOST R 34.10-2012 (256 bit) ParamSet B', 'id-tc26-gost-3410-2012-256-paramSetB'), + '1.2.643.7.1.2.1.1.3': ('GOST R 34.10-2012 (256 bit) ParamSet C', 'id-tc26-gost-3410-2012-256-paramSetC'), + '1.2.643.7.1.2.1.1.4': ('GOST R 34.10-2012 (256 bit) ParamSet D', 'id-tc26-gost-3410-2012-256-paramSetD'), + '1.2.643.7.1.2.1.2': ('id-tc26-gost-3410-2012-512-constants', ), + '1.2.643.7.1.2.1.2.0': ('GOST R 34.10-2012 (512 bit) testing parameter set', 'id-tc26-gost-3410-2012-512-paramSetTest'), + '1.2.643.7.1.2.1.2.1': ('GOST R 34.10-2012 (512 bit) ParamSet A', 'id-tc26-gost-3410-2012-512-paramSetA'), + '1.2.643.7.1.2.1.2.2': ('GOST R 34.10-2012 (512 bit) ParamSet B', 'id-tc26-gost-3410-2012-512-paramSetB'), + '1.2.643.7.1.2.1.2.3': ('GOST R 34.10-2012 (512 bit) ParamSet C', 'id-tc26-gost-3410-2012-512-paramSetC'), + '1.2.643.7.1.2.2': ('id-tc26-digest-constants', ), + '1.2.643.7.1.2.5': ('id-tc26-cipher-constants', ), + '1.2.643.7.1.2.5.1': ('id-tc26-gost-28147-constants', ), + '1.2.643.7.1.2.5.1.1': ('GOST 28147-89 TC26 parameter set', 'id-tc26-gost-28147-param-Z'), + '1.2.643.100.1': ('OGRN', 'OGRN'), + '1.2.643.100.3': ('SNILS', 'SNILS'), + '1.2.643.100.111': ('Signing Tool of Subject', 'subjectSignTool'), + '1.2.643.100.112': ('Signing Tool of Issuer', 'issuerSignTool'), + '1.2.804': ('ISO-UA', ), + '1.2.804.2.1.1.1': ('ua-pki', ), + '1.2.804.2.1.1.1.1.1.1': ('DSTU Gost 28147-2009', 'dstu28147'), + '1.2.804.2.1.1.1.1.1.1.2': ('DSTU Gost 28147-2009 OFB mode', 'dstu28147-ofb'), + '1.2.804.2.1.1.1.1.1.1.3': ('DSTU Gost 28147-2009 CFB mode', 'dstu28147-cfb'), + '1.2.804.2.1.1.1.1.1.1.5': ('DSTU Gost 28147-2009 key wrap', 'dstu28147-wrap'), + '1.2.804.2.1.1.1.1.1.2': ('HMAC DSTU Gost 34311-95', 'hmacWithDstu34311'), + '1.2.804.2.1.1.1.1.2.1': ('DSTU Gost 34311-95', 'dstu34311'), + '1.2.804.2.1.1.1.1.3.1.1': ('DSTU 4145-2002 little endian', 'dstu4145le'), + '1.2.804.2.1.1.1.1.3.1.1.1.1': ('DSTU 4145-2002 big endian', 'dstu4145be'), + '1.2.804.2.1.1.1.1.3.1.1.2.0': ('DSTU curve 0', 'uacurve0'), + '1.2.804.2.1.1.1.1.3.1.1.2.1': ('DSTU curve 1', 'uacurve1'), + '1.2.804.2.1.1.1.1.3.1.1.2.2': ('DSTU curve 2', 'uacurve2'), + '1.2.804.2.1.1.1.1.3.1.1.2.3': ('DSTU curve 3', 'uacurve3'), + '1.2.804.2.1.1.1.1.3.1.1.2.4': ('DSTU curve 4', 'uacurve4'), + '1.2.804.2.1.1.1.1.3.1.1.2.5': ('DSTU curve 5', 'uacurve5'), + '1.2.804.2.1.1.1.1.3.1.1.2.6': ('DSTU curve 6', 'uacurve6'), + '1.2.804.2.1.1.1.1.3.1.1.2.7': ('DSTU curve 7', 'uacurve7'), + '1.2.804.2.1.1.1.1.3.1.1.2.8': ('DSTU curve 8', 'uacurve8'), + '1.2.804.2.1.1.1.1.3.1.1.2.9': ('DSTU curve 9', 'uacurve9'), + '1.2.840': ('ISO US Member Body', 'ISO-US'), + '1.2.840.10040': ('X9.57', 'X9-57'), + '1.2.840.10040.2': ('holdInstruction', ), + '1.2.840.10040.2.1': ('Hold Instruction None', 'holdInstructionNone'), + '1.2.840.10040.2.2': ('Hold Instruction Call Issuer', 'holdInstructionCallIssuer'), + '1.2.840.10040.2.3': ('Hold Instruction Reject', 'holdInstructionReject'), + '1.2.840.10040.4': ('X9.57 CM ?', 'X9cm'), + '1.2.840.10040.4.1': ('dsaEncryption', 'DSA'), + '1.2.840.10040.4.3': ('dsaWithSHA1', 'DSA-SHA1'), + '1.2.840.10045': ('ANSI X9.62', 'ansi-X9-62'), + '1.2.840.10045.1': ('id-fieldType', ), + '1.2.840.10045.1.1': ('prime-field', ), + '1.2.840.10045.1.2': ('characteristic-two-field', ), + '1.2.840.10045.1.2.3': ('id-characteristic-two-basis', ), + '1.2.840.10045.1.2.3.1': ('onBasis', ), + '1.2.840.10045.1.2.3.2': ('tpBasis', ), + '1.2.840.10045.1.2.3.3': ('ppBasis', ), + '1.2.840.10045.2': ('id-publicKeyType', ), + '1.2.840.10045.2.1': ('id-ecPublicKey', ), + '1.2.840.10045.3': ('ellipticCurve', ), + '1.2.840.10045.3.0': ('c-TwoCurve', ), + '1.2.840.10045.3.0.1': ('c2pnb163v1', ), + '1.2.840.10045.3.0.2': ('c2pnb163v2', ), + '1.2.840.10045.3.0.3': ('c2pnb163v3', ), + '1.2.840.10045.3.0.4': ('c2pnb176v1', ), + '1.2.840.10045.3.0.5': ('c2tnb191v1', ), + '1.2.840.10045.3.0.6': ('c2tnb191v2', ), + '1.2.840.10045.3.0.7': ('c2tnb191v3', ), + '1.2.840.10045.3.0.8': ('c2onb191v4', ), + '1.2.840.10045.3.0.9': ('c2onb191v5', ), + '1.2.840.10045.3.0.10': ('c2pnb208w1', ), + '1.2.840.10045.3.0.11': ('c2tnb239v1', ), + '1.2.840.10045.3.0.12': ('c2tnb239v2', ), + '1.2.840.10045.3.0.13': ('c2tnb239v3', ), + '1.2.840.10045.3.0.14': ('c2onb239v4', ), + '1.2.840.10045.3.0.15': ('c2onb239v5', ), + '1.2.840.10045.3.0.16': ('c2pnb272w1', ), + '1.2.840.10045.3.0.17': ('c2pnb304w1', ), + '1.2.840.10045.3.0.18': ('c2tnb359v1', ), + '1.2.840.10045.3.0.19': ('c2pnb368w1', ), + '1.2.840.10045.3.0.20': ('c2tnb431r1', ), + '1.2.840.10045.3.1': ('primeCurve', ), + '1.2.840.10045.3.1.1': ('prime192v1', ), + '1.2.840.10045.3.1.2': ('prime192v2', ), + '1.2.840.10045.3.1.3': ('prime192v3', ), + '1.2.840.10045.3.1.4': ('prime239v1', ), + '1.2.840.10045.3.1.5': ('prime239v2', ), + '1.2.840.10045.3.1.6': ('prime239v3', ), + '1.2.840.10045.3.1.7': ('prime256v1', ), + '1.2.840.10045.4': ('id-ecSigType', ), + '1.2.840.10045.4.1': ('ecdsa-with-SHA1', ), + '1.2.840.10045.4.2': ('ecdsa-with-Recommended', ), + '1.2.840.10045.4.3': ('ecdsa-with-Specified', ), + '1.2.840.10045.4.3.1': ('ecdsa-with-SHA224', ), + '1.2.840.10045.4.3.2': ('ecdsa-with-SHA256', ), + '1.2.840.10045.4.3.3': ('ecdsa-with-SHA384', ), + '1.2.840.10045.4.3.4': ('ecdsa-with-SHA512', ), + '1.2.840.10046.2.1': ('X9.42 DH', 'dhpublicnumber'), + '1.2.840.113533.7.66.10': ('cast5-cbc', 'CAST5-CBC'), + '1.2.840.113533.7.66.12': ('pbeWithMD5AndCast5CBC', ), + '1.2.840.113533.7.66.13': ('password based MAC', 'id-PasswordBasedMAC'), + '1.2.840.113533.7.66.30': ('Diffie-Hellman based MAC', 'id-DHBasedMac'), + '1.2.840.113549': ('RSA Data Security, Inc.', 'rsadsi'), + '1.2.840.113549.1': ('RSA Data Security, Inc. PKCS', 'pkcs'), + '1.2.840.113549.1.1': ('pkcs1', ), + '1.2.840.113549.1.1.1': ('rsaEncryption', ), + '1.2.840.113549.1.1.2': ('md2WithRSAEncryption', 'RSA-MD2'), + '1.2.840.113549.1.1.3': ('md4WithRSAEncryption', 'RSA-MD4'), + '1.2.840.113549.1.1.4': ('md5WithRSAEncryption', 'RSA-MD5'), + '1.2.840.113549.1.1.5': ('sha1WithRSAEncryption', 'RSA-SHA1'), + '1.2.840.113549.1.1.6': ('rsaOAEPEncryptionSET', ), + '1.2.840.113549.1.1.7': ('rsaesOaep', 'RSAES-OAEP'), + '1.2.840.113549.1.1.8': ('mgf1', 'MGF1'), + '1.2.840.113549.1.1.9': ('pSpecified', 'PSPECIFIED'), + '1.2.840.113549.1.1.10': ('rsassaPss', 'RSASSA-PSS'), + '1.2.840.113549.1.1.11': ('sha256WithRSAEncryption', 'RSA-SHA256'), + '1.2.840.113549.1.1.12': ('sha384WithRSAEncryption', 'RSA-SHA384'), + '1.2.840.113549.1.1.13': ('sha512WithRSAEncryption', 'RSA-SHA512'), + '1.2.840.113549.1.1.14': ('sha224WithRSAEncryption', 'RSA-SHA224'), + '1.2.840.113549.1.1.15': ('sha512-224WithRSAEncryption', 'RSA-SHA512/224'), + '1.2.840.113549.1.1.16': ('sha512-256WithRSAEncryption', 'RSA-SHA512/256'), + '1.2.840.113549.1.3': ('pkcs3', ), + '1.2.840.113549.1.3.1': ('dhKeyAgreement', ), + '1.2.840.113549.1.5': ('pkcs5', ), + '1.2.840.113549.1.5.1': ('pbeWithMD2AndDES-CBC', 'PBE-MD2-DES'), + '1.2.840.113549.1.5.3': ('pbeWithMD5AndDES-CBC', 'PBE-MD5-DES'), + '1.2.840.113549.1.5.4': ('pbeWithMD2AndRC2-CBC', 'PBE-MD2-RC2-64'), + '1.2.840.113549.1.5.6': ('pbeWithMD5AndRC2-CBC', 'PBE-MD5-RC2-64'), + '1.2.840.113549.1.5.10': ('pbeWithSHA1AndDES-CBC', 'PBE-SHA1-DES'), + '1.2.840.113549.1.5.11': ('pbeWithSHA1AndRC2-CBC', 'PBE-SHA1-RC2-64'), + '1.2.840.113549.1.5.12': ('PBKDF2', ), + '1.2.840.113549.1.5.13': ('PBES2', ), + '1.2.840.113549.1.5.14': ('PBMAC1', ), + '1.2.840.113549.1.7': ('pkcs7', ), + '1.2.840.113549.1.7.1': ('pkcs7-data', ), + '1.2.840.113549.1.7.2': ('pkcs7-signedData', ), + '1.2.840.113549.1.7.3': ('pkcs7-envelopedData', ), + '1.2.840.113549.1.7.4': ('pkcs7-signedAndEnvelopedData', ), + '1.2.840.113549.1.7.5': ('pkcs7-digestData', ), + '1.2.840.113549.1.7.6': ('pkcs7-encryptedData', ), + '1.2.840.113549.1.9': ('pkcs9', ), + '1.2.840.113549.1.9.1': ('emailAddress', ), + '1.2.840.113549.1.9.2': ('unstructuredName', ), + '1.2.840.113549.1.9.3': ('contentType', ), + '1.2.840.113549.1.9.4': ('messageDigest', ), + '1.2.840.113549.1.9.5': ('signingTime', ), + '1.2.840.113549.1.9.6': ('countersignature', ), + '1.2.840.113549.1.9.7': ('challengePassword', ), + '1.2.840.113549.1.9.8': ('unstructuredAddress', ), + '1.2.840.113549.1.9.9': ('extendedCertificateAttributes', ), + '1.2.840.113549.1.9.14': ('Extension Request', 'extReq'), + '1.2.840.113549.1.9.15': ('S/MIME Capabilities', 'SMIME-CAPS'), + '1.2.840.113549.1.9.16': ('S/MIME', 'SMIME'), + '1.2.840.113549.1.9.16.0': ('id-smime-mod', ), + '1.2.840.113549.1.9.16.0.1': ('id-smime-mod-cms', ), + '1.2.840.113549.1.9.16.0.2': ('id-smime-mod-ess', ), + '1.2.840.113549.1.9.16.0.3': ('id-smime-mod-oid', ), + '1.2.840.113549.1.9.16.0.4': ('id-smime-mod-msg-v3', ), + '1.2.840.113549.1.9.16.0.5': ('id-smime-mod-ets-eSignature-88', ), + '1.2.840.113549.1.9.16.0.6': ('id-smime-mod-ets-eSignature-97', ), + '1.2.840.113549.1.9.16.0.7': ('id-smime-mod-ets-eSigPolicy-88', ), + '1.2.840.113549.1.9.16.0.8': ('id-smime-mod-ets-eSigPolicy-97', ), + '1.2.840.113549.1.9.16.1': ('id-smime-ct', ), + '1.2.840.113549.1.9.16.1.1': ('id-smime-ct-receipt', ), + '1.2.840.113549.1.9.16.1.2': ('id-smime-ct-authData', ), + '1.2.840.113549.1.9.16.1.3': ('id-smime-ct-publishCert', ), + '1.2.840.113549.1.9.16.1.4': ('id-smime-ct-TSTInfo', ), + '1.2.840.113549.1.9.16.1.5': ('id-smime-ct-TDTInfo', ), + '1.2.840.113549.1.9.16.1.6': ('id-smime-ct-contentInfo', ), + '1.2.840.113549.1.9.16.1.7': ('id-smime-ct-DVCSRequestData', ), + '1.2.840.113549.1.9.16.1.8': ('id-smime-ct-DVCSResponseData', ), + '1.2.840.113549.1.9.16.1.9': ('id-smime-ct-compressedData', ), + '1.2.840.113549.1.9.16.1.19': ('id-smime-ct-contentCollection', ), + '1.2.840.113549.1.9.16.1.23': ('id-smime-ct-authEnvelopedData', ), + '1.2.840.113549.1.9.16.1.27': ('id-ct-asciiTextWithCRLF', ), + '1.2.840.113549.1.9.16.1.28': ('id-ct-xml', ), + '1.2.840.113549.1.9.16.2': ('id-smime-aa', ), + '1.2.840.113549.1.9.16.2.1': ('id-smime-aa-receiptRequest', ), + '1.2.840.113549.1.9.16.2.2': ('id-smime-aa-securityLabel', ), + '1.2.840.113549.1.9.16.2.3': ('id-smime-aa-mlExpandHistory', ), + '1.2.840.113549.1.9.16.2.4': ('id-smime-aa-contentHint', ), + '1.2.840.113549.1.9.16.2.5': ('id-smime-aa-msgSigDigest', ), + '1.2.840.113549.1.9.16.2.6': ('id-smime-aa-encapContentType', ), + '1.2.840.113549.1.9.16.2.7': ('id-smime-aa-contentIdentifier', ), + '1.2.840.113549.1.9.16.2.8': ('id-smime-aa-macValue', ), + '1.2.840.113549.1.9.16.2.9': ('id-smime-aa-equivalentLabels', ), + '1.2.840.113549.1.9.16.2.10': ('id-smime-aa-contentReference', ), + '1.2.840.113549.1.9.16.2.11': ('id-smime-aa-encrypKeyPref', ), + '1.2.840.113549.1.9.16.2.12': ('id-smime-aa-signingCertificate', ), + '1.2.840.113549.1.9.16.2.13': ('id-smime-aa-smimeEncryptCerts', ), + '1.2.840.113549.1.9.16.2.14': ('id-smime-aa-timeStampToken', ), + '1.2.840.113549.1.9.16.2.15': ('id-smime-aa-ets-sigPolicyId', ), + '1.2.840.113549.1.9.16.2.16': ('id-smime-aa-ets-commitmentType', ), + '1.2.840.113549.1.9.16.2.17': ('id-smime-aa-ets-signerLocation', ), + '1.2.840.113549.1.9.16.2.18': ('id-smime-aa-ets-signerAttr', ), + '1.2.840.113549.1.9.16.2.19': ('id-smime-aa-ets-otherSigCert', ), + '1.2.840.113549.1.9.16.2.20': ('id-smime-aa-ets-contentTimestamp', ), + '1.2.840.113549.1.9.16.2.21': ('id-smime-aa-ets-CertificateRefs', ), + '1.2.840.113549.1.9.16.2.22': ('id-smime-aa-ets-RevocationRefs', ), + '1.2.840.113549.1.9.16.2.23': ('id-smime-aa-ets-certValues', ), + '1.2.840.113549.1.9.16.2.24': ('id-smime-aa-ets-revocationValues', ), + '1.2.840.113549.1.9.16.2.25': ('id-smime-aa-ets-escTimeStamp', ), + '1.2.840.113549.1.9.16.2.26': ('id-smime-aa-ets-certCRLTimestamp', ), + '1.2.840.113549.1.9.16.2.27': ('id-smime-aa-ets-archiveTimeStamp', ), + '1.2.840.113549.1.9.16.2.28': ('id-smime-aa-signatureType', ), + '1.2.840.113549.1.9.16.2.29': ('id-smime-aa-dvcs-dvc', ), + '1.2.840.113549.1.9.16.2.47': ('id-smime-aa-signingCertificateV2', ), + '1.2.840.113549.1.9.16.3': ('id-smime-alg', ), + '1.2.840.113549.1.9.16.3.1': ('id-smime-alg-ESDHwith3DES', ), + '1.2.840.113549.1.9.16.3.2': ('id-smime-alg-ESDHwithRC2', ), + '1.2.840.113549.1.9.16.3.3': ('id-smime-alg-3DESwrap', ), + '1.2.840.113549.1.9.16.3.4': ('id-smime-alg-RC2wrap', ), + '1.2.840.113549.1.9.16.3.5': ('id-smime-alg-ESDH', ), + '1.2.840.113549.1.9.16.3.6': ('id-smime-alg-CMS3DESwrap', ), + '1.2.840.113549.1.9.16.3.7': ('id-smime-alg-CMSRC2wrap', ), + '1.2.840.113549.1.9.16.3.8': ('zlib compression', 'ZLIB'), + '1.2.840.113549.1.9.16.3.9': ('id-alg-PWRI-KEK', ), + '1.2.840.113549.1.9.16.4': ('id-smime-cd', ), + '1.2.840.113549.1.9.16.4.1': ('id-smime-cd-ldap', ), + '1.2.840.113549.1.9.16.5': ('id-smime-spq', ), + '1.2.840.113549.1.9.16.5.1': ('id-smime-spq-ets-sqt-uri', ), + '1.2.840.113549.1.9.16.5.2': ('id-smime-spq-ets-sqt-unotice', ), + '1.2.840.113549.1.9.16.6': ('id-smime-cti', ), + '1.2.840.113549.1.9.16.6.1': ('id-smime-cti-ets-proofOfOrigin', ), + '1.2.840.113549.1.9.16.6.2': ('id-smime-cti-ets-proofOfReceipt', ), + '1.2.840.113549.1.9.16.6.3': ('id-smime-cti-ets-proofOfDelivery', ), + '1.2.840.113549.1.9.16.6.4': ('id-smime-cti-ets-proofOfSender', ), + '1.2.840.113549.1.9.16.6.5': ('id-smime-cti-ets-proofOfApproval', ), + '1.2.840.113549.1.9.16.6.6': ('id-smime-cti-ets-proofOfCreation', ), + '1.2.840.113549.1.9.20': ('friendlyName', ), + '1.2.840.113549.1.9.21': ('localKeyID', ), + '1.2.840.113549.1.9.22': ('certTypes', ), + '1.2.840.113549.1.9.22.1': ('x509Certificate', ), + '1.2.840.113549.1.9.22.2': ('sdsiCertificate', ), + '1.2.840.113549.1.9.23': ('crlTypes', ), + '1.2.840.113549.1.9.23.1': ('x509Crl', ), + '1.2.840.113549.1.12': ('pkcs12', ), + '1.2.840.113549.1.12.1': ('pkcs12-pbeids', ), + '1.2.840.113549.1.12.1.1': ('pbeWithSHA1And128BitRC4', 'PBE-SHA1-RC4-128'), + '1.2.840.113549.1.12.1.2': ('pbeWithSHA1And40BitRC4', 'PBE-SHA1-RC4-40'), + '1.2.840.113549.1.12.1.3': ('pbeWithSHA1And3-KeyTripleDES-CBC', 'PBE-SHA1-3DES'), + '1.2.840.113549.1.12.1.4': ('pbeWithSHA1And2-KeyTripleDES-CBC', 'PBE-SHA1-2DES'), + '1.2.840.113549.1.12.1.5': ('pbeWithSHA1And128BitRC2-CBC', 'PBE-SHA1-RC2-128'), + '1.2.840.113549.1.12.1.6': ('pbeWithSHA1And40BitRC2-CBC', 'PBE-SHA1-RC2-40'), + '1.2.840.113549.1.12.10': ('pkcs12-Version1', ), + '1.2.840.113549.1.12.10.1': ('pkcs12-BagIds', ), + '1.2.840.113549.1.12.10.1.1': ('keyBag', ), + '1.2.840.113549.1.12.10.1.2': ('pkcs8ShroudedKeyBag', ), + '1.2.840.113549.1.12.10.1.3': ('certBag', ), + '1.2.840.113549.1.12.10.1.4': ('crlBag', ), + '1.2.840.113549.1.12.10.1.5': ('secretBag', ), + '1.2.840.113549.1.12.10.1.6': ('safeContentsBag', ), + '1.2.840.113549.2.2': ('md2', 'MD2'), + '1.2.840.113549.2.4': ('md4', 'MD4'), + '1.2.840.113549.2.5': ('md5', 'MD5'), + '1.2.840.113549.2.6': ('hmacWithMD5', ), + '1.2.840.113549.2.7': ('hmacWithSHA1', ), + '1.2.840.113549.2.8': ('hmacWithSHA224', ), + '1.2.840.113549.2.9': ('hmacWithSHA256', ), + '1.2.840.113549.2.10': ('hmacWithSHA384', ), + '1.2.840.113549.2.11': ('hmacWithSHA512', ), + '1.2.840.113549.2.12': ('hmacWithSHA512-224', ), + '1.2.840.113549.2.13': ('hmacWithSHA512-256', ), + '1.2.840.113549.3.2': ('rc2-cbc', 'RC2-CBC'), + '1.2.840.113549.3.4': ('rc4', 'RC4'), + '1.2.840.113549.3.7': ('des-ede3-cbc', 'DES-EDE3-CBC'), + '1.2.840.113549.3.8': ('rc5-cbc', 'RC5-CBC'), + '1.2.840.113549.3.10': ('des-cdmf', 'DES-CDMF'), + '1.3': ('identified-organization', 'org', 'ORG'), + '1.3.6': ('dod', 'DOD'), + '1.3.6.1': ('iana', 'IANA', 'internet'), + '1.3.6.1.1': ('Directory', 'directory'), + '1.3.6.1.2': ('Management', 'mgmt'), + '1.3.6.1.3': ('Experimental', 'experimental'), + '1.3.6.1.4': ('Private', 'private'), + '1.3.6.1.4.1': ('Enterprises', 'enterprises'), + '1.3.6.1.4.1.188.7.1.1.2': ('idea-cbc', 'IDEA-CBC'), + '1.3.6.1.4.1.311.2.1.14': ('Microsoft Extension Request', 'msExtReq'), + '1.3.6.1.4.1.311.2.1.21': ('Microsoft Individual Code Signing', 'msCodeInd'), + '1.3.6.1.4.1.311.2.1.22': ('Microsoft Commercial Code Signing', 'msCodeCom'), + '1.3.6.1.4.1.311.10.3.1': ('Microsoft Trust List Signing', 'msCTLSign'), + '1.3.6.1.4.1.311.10.3.3': ('Microsoft Server Gated Crypto', 'msSGC'), + '1.3.6.1.4.1.311.10.3.4': ('Microsoft Encrypted File System', 'msEFS'), + '1.3.6.1.4.1.311.17.1': ('Microsoft CSP Name', 'CSPName'), + '1.3.6.1.4.1.311.17.2': ('Microsoft Local Key set', 'LocalKeySet'), + '1.3.6.1.4.1.311.20.2.2': ('Microsoft Smartcardlogin', 'msSmartcardLogin'), + '1.3.6.1.4.1.311.20.2.3': ('Microsoft Universal Principal Name', 'msUPN'), + '1.3.6.1.4.1.311.60.2.1.1': ('jurisdictionLocalityName', 'jurisdictionL'), + '1.3.6.1.4.1.311.60.2.1.2': ('jurisdictionStateOrProvinceName', 'jurisdictionST'), + '1.3.6.1.4.1.311.60.2.1.3': ('jurisdictionCountryName', 'jurisdictionC'), + '1.3.6.1.4.1.1466.344': ('dcObject', 'dcobject'), + '1.3.6.1.4.1.1722.12.2.1.16': ('blake2b512', 'BLAKE2b512'), + '1.3.6.1.4.1.1722.12.2.2.8': ('blake2s256', 'BLAKE2s256'), + '1.3.6.1.4.1.3029.1.2': ('bf-cbc', 'BF-CBC'), + '1.3.6.1.4.1.11129.2.4.2': ('CT Precertificate SCTs', 'ct_precert_scts'), + '1.3.6.1.4.1.11129.2.4.3': ('CT Precertificate Poison', 'ct_precert_poison'), + '1.3.6.1.4.1.11129.2.4.4': ('CT Precertificate Signer', 'ct_precert_signer'), + '1.3.6.1.4.1.11129.2.4.5': ('CT Certificate SCTs', 'ct_cert_scts'), + '1.3.6.1.4.1.11591.4.11': ('scrypt', 'id-scrypt'), + '1.3.6.1.5': ('Security', 'security'), + '1.3.6.1.5.2.3': ('id-pkinit', ), + '1.3.6.1.5.2.3.4': ('PKINIT Client Auth', 'pkInitClientAuth'), + '1.3.6.1.5.2.3.5': ('Signing KDC Response', 'pkInitKDC'), + '1.3.6.1.5.5.7': ('PKIX', ), + '1.3.6.1.5.5.7.0': ('id-pkix-mod', ), + '1.3.6.1.5.5.7.0.1': ('id-pkix1-explicit-88', ), + '1.3.6.1.5.5.7.0.2': ('id-pkix1-implicit-88', ), + '1.3.6.1.5.5.7.0.3': ('id-pkix1-explicit-93', ), + '1.3.6.1.5.5.7.0.4': ('id-pkix1-implicit-93', ), + '1.3.6.1.5.5.7.0.5': ('id-mod-crmf', ), + '1.3.6.1.5.5.7.0.6': ('id-mod-cmc', ), + '1.3.6.1.5.5.7.0.7': ('id-mod-kea-profile-88', ), + '1.3.6.1.5.5.7.0.8': ('id-mod-kea-profile-93', ), + '1.3.6.1.5.5.7.0.9': ('id-mod-cmp', ), + '1.3.6.1.5.5.7.0.10': ('id-mod-qualified-cert-88', ), + '1.3.6.1.5.5.7.0.11': ('id-mod-qualified-cert-93', ), + '1.3.6.1.5.5.7.0.12': ('id-mod-attribute-cert', ), + '1.3.6.1.5.5.7.0.13': ('id-mod-timestamp-protocol', ), + '1.3.6.1.5.5.7.0.14': ('id-mod-ocsp', ), + '1.3.6.1.5.5.7.0.15': ('id-mod-dvcs', ), + '1.3.6.1.5.5.7.0.16': ('id-mod-cmp2000', ), + '1.3.6.1.5.5.7.1': ('id-pe', ), + '1.3.6.1.5.5.7.1.1': ('Authority Information Access', 'authorityInfoAccess'), + '1.3.6.1.5.5.7.1.2': ('Biometric Info', 'biometricInfo'), + '1.3.6.1.5.5.7.1.3': ('qcStatements', ), + '1.3.6.1.5.5.7.1.4': ('ac-auditEntity', ), + '1.3.6.1.5.5.7.1.5': ('ac-targeting', ), + '1.3.6.1.5.5.7.1.6': ('aaControls', ), + '1.3.6.1.5.5.7.1.7': ('sbgp-ipAddrBlock', ), + '1.3.6.1.5.5.7.1.8': ('sbgp-autonomousSysNum', ), + '1.3.6.1.5.5.7.1.9': ('sbgp-routerIdentifier', ), + '1.3.6.1.5.5.7.1.10': ('ac-proxying', ), + '1.3.6.1.5.5.7.1.11': ('Subject Information Access', 'subjectInfoAccess'), + '1.3.6.1.5.5.7.1.14': ('Proxy Certificate Information', 'proxyCertInfo'), + '1.3.6.1.5.5.7.1.24': ('TLS Feature', 'tlsfeature'), + '1.3.6.1.5.5.7.2': ('id-qt', ), + '1.3.6.1.5.5.7.2.1': ('Policy Qualifier CPS', 'id-qt-cps'), + '1.3.6.1.5.5.7.2.2': ('Policy Qualifier User Notice', 'id-qt-unotice'), + '1.3.6.1.5.5.7.2.3': ('textNotice', ), + '1.3.6.1.5.5.7.3': ('id-kp', ), + '1.3.6.1.5.5.7.3.1': ('TLS Web Server Authentication', 'serverAuth'), + '1.3.6.1.5.5.7.3.2': ('TLS Web Client Authentication', 'clientAuth'), + '1.3.6.1.5.5.7.3.3': ('Code Signing', 'codeSigning'), + '1.3.6.1.5.5.7.3.4': ('E-mail Protection', 'emailProtection'), + '1.3.6.1.5.5.7.3.5': ('IPSec End System', 'ipsecEndSystem'), + '1.3.6.1.5.5.7.3.6': ('IPSec Tunnel', 'ipsecTunnel'), + '1.3.6.1.5.5.7.3.7': ('IPSec User', 'ipsecUser'), + '1.3.6.1.5.5.7.3.8': ('Time Stamping', 'timeStamping'), + '1.3.6.1.5.5.7.3.9': ('OCSP Signing', 'OCSPSigning'), + '1.3.6.1.5.5.7.3.10': ('dvcs', 'DVCS'), + '1.3.6.1.5.5.7.3.17': ('ipsec Internet Key Exchange', 'ipsecIKE'), + '1.3.6.1.5.5.7.3.18': ('Ctrl/provision WAP Access', 'capwapAC'), + '1.3.6.1.5.5.7.3.19': ('Ctrl/Provision WAP Termination', 'capwapWTP'), + '1.3.6.1.5.5.7.3.21': ('SSH Client', 'secureShellClient'), + '1.3.6.1.5.5.7.3.22': ('SSH Server', 'secureShellServer'), + '1.3.6.1.5.5.7.3.23': ('Send Router', 'sendRouter'), + '1.3.6.1.5.5.7.3.24': ('Send Proxied Router', 'sendProxiedRouter'), + '1.3.6.1.5.5.7.3.25': ('Send Owner', 'sendOwner'), + '1.3.6.1.5.5.7.3.26': ('Send Proxied Owner', 'sendProxiedOwner'), + '1.3.6.1.5.5.7.3.27': ('CMC Certificate Authority', 'cmcCA'), + '1.3.6.1.5.5.7.3.28': ('CMC Registration Authority', 'cmcRA'), + '1.3.6.1.5.5.7.4': ('id-it', ), + '1.3.6.1.5.5.7.4.1': ('id-it-caProtEncCert', ), + '1.3.6.1.5.5.7.4.2': ('id-it-signKeyPairTypes', ), + '1.3.6.1.5.5.7.4.3': ('id-it-encKeyPairTypes', ), + '1.3.6.1.5.5.7.4.4': ('id-it-preferredSymmAlg', ), + '1.3.6.1.5.5.7.4.5': ('id-it-caKeyUpdateInfo', ), + '1.3.6.1.5.5.7.4.6': ('id-it-currentCRL', ), + '1.3.6.1.5.5.7.4.7': ('id-it-unsupportedOIDs', ), + '1.3.6.1.5.5.7.4.8': ('id-it-subscriptionRequest', ), + '1.3.6.1.5.5.7.4.9': ('id-it-subscriptionResponse', ), + '1.3.6.1.5.5.7.4.10': ('id-it-keyPairParamReq', ), + '1.3.6.1.5.5.7.4.11': ('id-it-keyPairParamRep', ), + '1.3.6.1.5.5.7.4.12': ('id-it-revPassphrase', ), + '1.3.6.1.5.5.7.4.13': ('id-it-implicitConfirm', ), + '1.3.6.1.5.5.7.4.14': ('id-it-confirmWaitTime', ), + '1.3.6.1.5.5.7.4.15': ('id-it-origPKIMessage', ), + '1.3.6.1.5.5.7.4.16': ('id-it-suppLangTags', ), + '1.3.6.1.5.5.7.5': ('id-pkip', ), + '1.3.6.1.5.5.7.5.1': ('id-regCtrl', ), + '1.3.6.1.5.5.7.5.1.1': ('id-regCtrl-regToken', ), + '1.3.6.1.5.5.7.5.1.2': ('id-regCtrl-authenticator', ), + '1.3.6.1.5.5.7.5.1.3': ('id-regCtrl-pkiPublicationInfo', ), + '1.3.6.1.5.5.7.5.1.4': ('id-regCtrl-pkiArchiveOptions', ), + '1.3.6.1.5.5.7.5.1.5': ('id-regCtrl-oldCertID', ), + '1.3.6.1.5.5.7.5.1.6': ('id-regCtrl-protocolEncrKey', ), + '1.3.6.1.5.5.7.5.2': ('id-regInfo', ), + '1.3.6.1.5.5.7.5.2.1': ('id-regInfo-utf8Pairs', ), + '1.3.6.1.5.5.7.5.2.2': ('id-regInfo-certReq', ), + '1.3.6.1.5.5.7.6': ('id-alg', ), + '1.3.6.1.5.5.7.6.1': ('id-alg-des40', ), + '1.3.6.1.5.5.7.6.2': ('id-alg-noSignature', ), + '1.3.6.1.5.5.7.6.3': ('id-alg-dh-sig-hmac-sha1', ), + '1.3.6.1.5.5.7.6.4': ('id-alg-dh-pop', ), + '1.3.6.1.5.5.7.7': ('id-cmc', ), + '1.3.6.1.5.5.7.7.1': ('id-cmc-statusInfo', ), + '1.3.6.1.5.5.7.7.2': ('id-cmc-identification', ), + '1.3.6.1.5.5.7.7.3': ('id-cmc-identityProof', ), + '1.3.6.1.5.5.7.7.4': ('id-cmc-dataReturn', ), + '1.3.6.1.5.5.7.7.5': ('id-cmc-transactionId', ), + '1.3.6.1.5.5.7.7.6': ('id-cmc-senderNonce', ), + '1.3.6.1.5.5.7.7.7': ('id-cmc-recipientNonce', ), + '1.3.6.1.5.5.7.7.8': ('id-cmc-addExtensions', ), + '1.3.6.1.5.5.7.7.9': ('id-cmc-encryptedPOP', ), + '1.3.6.1.5.5.7.7.10': ('id-cmc-decryptedPOP', ), + '1.3.6.1.5.5.7.7.11': ('id-cmc-lraPOPWitness', ), + '1.3.6.1.5.5.7.7.15': ('id-cmc-getCert', ), + '1.3.6.1.5.5.7.7.16': ('id-cmc-getCRL', ), + '1.3.6.1.5.5.7.7.17': ('id-cmc-revokeRequest', ), + '1.3.6.1.5.5.7.7.18': ('id-cmc-regInfo', ), + '1.3.6.1.5.5.7.7.19': ('id-cmc-responseInfo', ), + '1.3.6.1.5.5.7.7.21': ('id-cmc-queryPending', ), + '1.3.6.1.5.5.7.7.22': ('id-cmc-popLinkRandom', ), + '1.3.6.1.5.5.7.7.23': ('id-cmc-popLinkWitness', ), + '1.3.6.1.5.5.7.7.24': ('id-cmc-confirmCertAcceptance', ), + '1.3.6.1.5.5.7.8': ('id-on', ), + '1.3.6.1.5.5.7.8.1': ('id-on-personalData', ), + '1.3.6.1.5.5.7.8.3': ('Permanent Identifier', 'id-on-permanentIdentifier'), + '1.3.6.1.5.5.7.9': ('id-pda', ), + '1.3.6.1.5.5.7.9.1': ('id-pda-dateOfBirth', ), + '1.3.6.1.5.5.7.9.2': ('id-pda-placeOfBirth', ), + '1.3.6.1.5.5.7.9.3': ('id-pda-gender', ), + '1.3.6.1.5.5.7.9.4': ('id-pda-countryOfCitizenship', ), + '1.3.6.1.5.5.7.9.5': ('id-pda-countryOfResidence', ), + '1.3.6.1.5.5.7.10': ('id-aca', ), + '1.3.6.1.5.5.7.10.1': ('id-aca-authenticationInfo', ), + '1.3.6.1.5.5.7.10.2': ('id-aca-accessIdentity', ), + '1.3.6.1.5.5.7.10.3': ('id-aca-chargingIdentity', ), + '1.3.6.1.5.5.7.10.4': ('id-aca-group', ), + '1.3.6.1.5.5.7.10.5': ('id-aca-role', ), + '1.3.6.1.5.5.7.10.6': ('id-aca-encAttrs', ), + '1.3.6.1.5.5.7.11': ('id-qcs', ), + '1.3.6.1.5.5.7.11.1': ('id-qcs-pkixQCSyntax-v1', ), + '1.3.6.1.5.5.7.12': ('id-cct', ), + '1.3.6.1.5.5.7.12.1': ('id-cct-crs', ), + '1.3.6.1.5.5.7.12.2': ('id-cct-PKIData', ), + '1.3.6.1.5.5.7.12.3': ('id-cct-PKIResponse', ), + '1.3.6.1.5.5.7.21': ('id-ppl', ), + '1.3.6.1.5.5.7.21.0': ('Any language', 'id-ppl-anyLanguage'), + '1.3.6.1.5.5.7.21.1': ('Inherit all', 'id-ppl-inheritAll'), + '1.3.6.1.5.5.7.21.2': ('Independent', 'id-ppl-independent'), + '1.3.6.1.5.5.7.48': ('id-ad', ), + '1.3.6.1.5.5.7.48.1': ('OCSP', 'OCSP', 'id-pkix-OCSP'), + '1.3.6.1.5.5.7.48.1.1': ('Basic OCSP Response', 'basicOCSPResponse'), + '1.3.6.1.5.5.7.48.1.2': ('OCSP Nonce', 'Nonce'), + '1.3.6.1.5.5.7.48.1.3': ('OCSP CRL ID', 'CrlID'), + '1.3.6.1.5.5.7.48.1.4': ('Acceptable OCSP Responses', 'acceptableResponses'), + '1.3.6.1.5.5.7.48.1.5': ('OCSP No Check', 'noCheck'), + '1.3.6.1.5.5.7.48.1.6': ('OCSP Archive Cutoff', 'archiveCutoff'), + '1.3.6.1.5.5.7.48.1.7': ('OCSP Service Locator', 'serviceLocator'), + '1.3.6.1.5.5.7.48.1.8': ('Extended OCSP Status', 'extendedStatus'), + '1.3.6.1.5.5.7.48.1.9': ('valid', ), + '1.3.6.1.5.5.7.48.1.10': ('path', ), + '1.3.6.1.5.5.7.48.1.11': ('Trust Root', 'trustRoot'), + '1.3.6.1.5.5.7.48.2': ('CA Issuers', 'caIssuers'), + '1.3.6.1.5.5.7.48.3': ('AD Time Stamping', 'ad_timestamping'), + '1.3.6.1.5.5.7.48.4': ('ad dvcs', 'AD_DVCS'), + '1.3.6.1.5.5.7.48.5': ('CA Repository', 'caRepository'), + '1.3.6.1.5.5.8.1.1': ('hmac-md5', 'HMAC-MD5'), + '1.3.6.1.5.5.8.1.2': ('hmac-sha1', 'HMAC-SHA1'), + '1.3.6.1.6': ('SNMPv2', 'snmpv2'), + '1.3.6.1.7': ('Mail', ), + '1.3.6.1.7.1': ('MIME MHS', 'mime-mhs'), + '1.3.6.1.7.1.1': ('mime-mhs-headings', 'mime-mhs-headings'), + '1.3.6.1.7.1.1.1': ('id-hex-partial-message', 'id-hex-partial-message'), + '1.3.6.1.7.1.1.2': ('id-hex-multipart-message', 'id-hex-multipart-message'), + '1.3.6.1.7.1.2': ('mime-mhs-bodies', 'mime-mhs-bodies'), + '1.3.14.3.2': ('algorithm', 'algorithm'), + '1.3.14.3.2.3': ('md5WithRSA', 'RSA-NP-MD5'), + '1.3.14.3.2.6': ('des-ecb', 'DES-ECB'), + '1.3.14.3.2.7': ('des-cbc', 'DES-CBC'), + '1.3.14.3.2.8': ('des-ofb', 'DES-OFB'), + '1.3.14.3.2.9': ('des-cfb', 'DES-CFB'), + '1.3.14.3.2.11': ('rsaSignature', ), + '1.3.14.3.2.12': ('dsaEncryption-old', 'DSA-old'), + '1.3.14.3.2.13': ('dsaWithSHA', 'DSA-SHA'), + '1.3.14.3.2.15': ('shaWithRSAEncryption', 'RSA-SHA'), + '1.3.14.3.2.17': ('des-ede', 'DES-EDE'), + '1.3.14.3.2.18': ('sha', 'SHA'), + '1.3.14.3.2.26': ('sha1', 'SHA1'), + '1.3.14.3.2.27': ('dsaWithSHA1-old', 'DSA-SHA1-old'), + '1.3.14.3.2.29': ('sha1WithRSA', 'RSA-SHA1-2'), + '1.3.36.3.2.1': ('ripemd160', 'RIPEMD160'), + '1.3.36.3.3.1.2': ('ripemd160WithRSA', 'RSA-RIPEMD160'), + '1.3.36.3.3.2.8.1.1.1': ('brainpoolP160r1', ), + '1.3.36.3.3.2.8.1.1.2': ('brainpoolP160t1', ), + '1.3.36.3.3.2.8.1.1.3': ('brainpoolP192r1', ), + '1.3.36.3.3.2.8.1.1.4': ('brainpoolP192t1', ), + '1.3.36.3.3.2.8.1.1.5': ('brainpoolP224r1', ), + '1.3.36.3.3.2.8.1.1.6': ('brainpoolP224t1', ), + '1.3.36.3.3.2.8.1.1.7': ('brainpoolP256r1', ), + '1.3.36.3.3.2.8.1.1.8': ('brainpoolP256t1', ), + '1.3.36.3.3.2.8.1.1.9': ('brainpoolP320r1', ), + '1.3.36.3.3.2.8.1.1.10': ('brainpoolP320t1', ), + '1.3.36.3.3.2.8.1.1.11': ('brainpoolP384r1', ), + '1.3.36.3.3.2.8.1.1.12': ('brainpoolP384t1', ), + '1.3.36.3.3.2.8.1.1.13': ('brainpoolP512r1', ), + '1.3.36.3.3.2.8.1.1.14': ('brainpoolP512t1', ), + '1.3.36.8.3.3': ('Professional Information or basis for Admission', 'x509ExtAdmission'), + '1.3.101.1.4.1': ('Strong Extranet ID', 'SXNetID'), + '1.3.101.110': ('X25519', ), + '1.3.101.111': ('X448', ), + '1.3.101.112': ('ED25519', ), + '1.3.101.113': ('ED448', ), + '1.3.111': ('ieee', ), + '1.3.111.2.1619': ('IEEE Security in Storage Working Group', 'ieee-siswg'), + '1.3.111.2.1619.0.1.1': ('aes-128-xts', 'AES-128-XTS'), + '1.3.111.2.1619.0.1.2': ('aes-256-xts', 'AES-256-XTS'), + '1.3.132': ('certicom-arc', ), + '1.3.132.0': ('secg_ellipticCurve', ), + '1.3.132.0.1': ('sect163k1', ), + '1.3.132.0.2': ('sect163r1', ), + '1.3.132.0.3': ('sect239k1', ), + '1.3.132.0.4': ('sect113r1', ), + '1.3.132.0.5': ('sect113r2', ), + '1.3.132.0.6': ('secp112r1', ), + '1.3.132.0.7': ('secp112r2', ), + '1.3.132.0.8': ('secp160r1', ), + '1.3.132.0.9': ('secp160k1', ), + '1.3.132.0.10': ('secp256k1', ), + '1.3.132.0.15': ('sect163r2', ), + '1.3.132.0.16': ('sect283k1', ), + '1.3.132.0.17': ('sect283r1', ), + '1.3.132.0.22': ('sect131r1', ), + '1.3.132.0.23': ('sect131r2', ), + '1.3.132.0.24': ('sect193r1', ), + '1.3.132.0.25': ('sect193r2', ), + '1.3.132.0.26': ('sect233k1', ), + '1.3.132.0.27': ('sect233r1', ), + '1.3.132.0.28': ('secp128r1', ), + '1.3.132.0.29': ('secp128r2', ), + '1.3.132.0.30': ('secp160r2', ), + '1.3.132.0.31': ('secp192k1', ), + '1.3.132.0.32': ('secp224k1', ), + '1.3.132.0.33': ('secp224r1', ), + '1.3.132.0.34': ('secp384r1', ), + '1.3.132.0.35': ('secp521r1', ), + '1.3.132.0.36': ('sect409k1', ), + '1.3.132.0.37': ('sect409r1', ), + '1.3.132.0.38': ('sect571k1', ), + '1.3.132.0.39': ('sect571r1', ), + '1.3.132.1': ('secg-scheme', ), + '1.3.132.1.11.0': ('dhSinglePass-stdDH-sha224kdf-scheme', ), + '1.3.132.1.11.1': ('dhSinglePass-stdDH-sha256kdf-scheme', ), + '1.3.132.1.11.2': ('dhSinglePass-stdDH-sha384kdf-scheme', ), + '1.3.132.1.11.3': ('dhSinglePass-stdDH-sha512kdf-scheme', ), + '1.3.132.1.14.0': ('dhSinglePass-cofactorDH-sha224kdf-scheme', ), + '1.3.132.1.14.1': ('dhSinglePass-cofactorDH-sha256kdf-scheme', ), + '1.3.132.1.14.2': ('dhSinglePass-cofactorDH-sha384kdf-scheme', ), + '1.3.132.1.14.3': ('dhSinglePass-cofactorDH-sha512kdf-scheme', ), + '1.3.133.16.840.63.0': ('x9-63-scheme', ), + '1.3.133.16.840.63.0.2': ('dhSinglePass-stdDH-sha1kdf-scheme', ), + '1.3.133.16.840.63.0.3': ('dhSinglePass-cofactorDH-sha1kdf-scheme', ), + '2': ('joint-iso-itu-t', 'JOINT-ISO-ITU-T', 'joint-iso-ccitt'), + '2.5': ('directory services (X.500)', 'X500'), + '2.5.1.5': ('Selected Attribute Types', 'selected-attribute-types'), + '2.5.1.5.55': ('clearance', ), + '2.5.4': ('X509', ), + '2.5.4.3': ('commonName', 'CN'), + '2.5.4.4': ('surname', 'SN'), + '2.5.4.5': ('serialNumber', ), + '2.5.4.6': ('countryName', 'C'), + '2.5.4.7': ('localityName', 'L'), + '2.5.4.8': ('stateOrProvinceName', 'ST'), + '2.5.4.9': ('streetAddress', 'street'), + '2.5.4.10': ('organizationName', 'O'), + '2.5.4.11': ('organizationalUnitName', 'OU'), + '2.5.4.12': ('title', 'title'), + '2.5.4.13': ('description', ), + '2.5.4.14': ('searchGuide', ), + '2.5.4.15': ('businessCategory', ), + '2.5.4.16': ('postalAddress', ), + '2.5.4.17': ('postalCode', ), + '2.5.4.18': ('postOfficeBox', ), + '2.5.4.19': ('physicalDeliveryOfficeName', ), + '2.5.4.20': ('telephoneNumber', ), + '2.5.4.21': ('telexNumber', ), + '2.5.4.22': ('teletexTerminalIdentifier', ), + '2.5.4.23': ('facsimileTelephoneNumber', ), + '2.5.4.24': ('x121Address', ), + '2.5.4.25': ('internationaliSDNNumber', ), + '2.5.4.26': ('registeredAddress', ), + '2.5.4.27': ('destinationIndicator', ), + '2.5.4.28': ('preferredDeliveryMethod', ), + '2.5.4.29': ('presentationAddress', ), + '2.5.4.30': ('supportedApplicationContext', ), + '2.5.4.31': ('member', ), + '2.5.4.32': ('owner', ), + '2.5.4.33': ('roleOccupant', ), + '2.5.4.34': ('seeAlso', ), + '2.5.4.35': ('userPassword', ), + '2.5.4.36': ('userCertificate', ), + '2.5.4.37': ('cACertificate', ), + '2.5.4.38': ('authorityRevocationList', ), + '2.5.4.39': ('certificateRevocationList', ), + '2.5.4.40': ('crossCertificatePair', ), + '2.5.4.41': ('name', 'name'), + '2.5.4.42': ('givenName', 'GN'), + '2.5.4.43': ('initials', 'initials'), + '2.5.4.44': ('generationQualifier', ), + '2.5.4.45': ('x500UniqueIdentifier', ), + '2.5.4.46': ('dnQualifier', 'dnQualifier'), + '2.5.4.47': ('enhancedSearchGuide', ), + '2.5.4.48': ('protocolInformation', ), + '2.5.4.49': ('distinguishedName', ), + '2.5.4.50': ('uniqueMember', ), + '2.5.4.51': ('houseIdentifier', ), + '2.5.4.52': ('supportedAlgorithms', ), + '2.5.4.53': ('deltaRevocationList', ), + '2.5.4.54': ('dmdName', ), + '2.5.4.65': ('pseudonym', ), + '2.5.4.72': ('role', 'role'), + '2.5.4.97': ('organizationIdentifier', ), + '2.5.4.98': ('countryCode3c', 'c3'), + '2.5.4.99': ('countryCode3n', 'n3'), + '2.5.4.100': ('dnsName', ), + '2.5.8': ('directory services - algorithms', 'X500algorithms'), + '2.5.8.1.1': ('rsa', 'RSA'), + '2.5.8.3.100': ('mdc2WithRSA', 'RSA-MDC2'), + '2.5.8.3.101': ('mdc2', 'MDC2'), + '2.5.29': ('id-ce', ), + '2.5.29.9': ('X509v3 Subject Directory Attributes', 'subjectDirectoryAttributes'), + '2.5.29.14': ('X509v3 Subject Key Identifier', 'subjectKeyIdentifier'), + '2.5.29.15': ('X509v3 Key Usage', 'keyUsage'), + '2.5.29.16': ('X509v3 Private Key Usage Period', 'privateKeyUsagePeriod'), + '2.5.29.17': ('X509v3 Subject Alternative Name', 'subjectAltName'), + '2.5.29.18': ('X509v3 Issuer Alternative Name', 'issuerAltName'), + '2.5.29.19': ('X509v3 Basic Constraints', 'basicConstraints'), + '2.5.29.20': ('X509v3 CRL Number', 'crlNumber'), + '2.5.29.21': ('X509v3 CRL Reason Code', 'CRLReason'), + '2.5.29.23': ('Hold Instruction Code', 'holdInstructionCode'), + '2.5.29.24': ('Invalidity Date', 'invalidityDate'), + '2.5.29.27': ('X509v3 Delta CRL Indicator', 'deltaCRL'), + '2.5.29.28': ('X509v3 Issuing Distribution Point', 'issuingDistributionPoint'), + '2.5.29.29': ('X509v3 Certificate Issuer', 'certificateIssuer'), + '2.5.29.30': ('X509v3 Name Constraints', 'nameConstraints'), + '2.5.29.31': ('X509v3 CRL Distribution Points', 'crlDistributionPoints'), + '2.5.29.32': ('X509v3 Certificate Policies', 'certificatePolicies'), + '2.5.29.32.0': ('X509v3 Any Policy', 'anyPolicy'), + '2.5.29.33': ('X509v3 Policy Mappings', 'policyMappings'), + '2.5.29.35': ('X509v3 Authority Key Identifier', 'authorityKeyIdentifier'), + '2.5.29.36': ('X509v3 Policy Constraints', 'policyConstraints'), + '2.5.29.37': ('X509v3 Extended Key Usage', 'extendedKeyUsage'), + '2.5.29.37.0': ('Any Extended Key Usage', 'anyExtendedKeyUsage'), + '2.5.29.46': ('X509v3 Freshest CRL', 'freshestCRL'), + '2.5.29.54': ('X509v3 Inhibit Any Policy', 'inhibitAnyPolicy'), + '2.5.29.55': ('X509v3 AC Targeting', 'targetInformation'), + '2.5.29.56': ('X509v3 No Revocation Available', 'noRevAvail'), + '2.16.840.1.101.3': ('csor', ), + '2.16.840.1.101.3.4': ('nistAlgorithms', ), + '2.16.840.1.101.3.4.1': ('aes', ), + '2.16.840.1.101.3.4.1.1': ('aes-128-ecb', 'AES-128-ECB'), + '2.16.840.1.101.3.4.1.2': ('aes-128-cbc', 'AES-128-CBC'), + '2.16.840.1.101.3.4.1.3': ('aes-128-ofb', 'AES-128-OFB'), + '2.16.840.1.101.3.4.1.4': ('aes-128-cfb', 'AES-128-CFB'), + '2.16.840.1.101.3.4.1.5': ('id-aes128-wrap', ), + '2.16.840.1.101.3.4.1.6': ('aes-128-gcm', 'id-aes128-GCM'), + '2.16.840.1.101.3.4.1.7': ('aes-128-ccm', 'id-aes128-CCM'), + '2.16.840.1.101.3.4.1.8': ('id-aes128-wrap-pad', ), + '2.16.840.1.101.3.4.1.21': ('aes-192-ecb', 'AES-192-ECB'), + '2.16.840.1.101.3.4.1.22': ('aes-192-cbc', 'AES-192-CBC'), + '2.16.840.1.101.3.4.1.23': ('aes-192-ofb', 'AES-192-OFB'), + '2.16.840.1.101.3.4.1.24': ('aes-192-cfb', 'AES-192-CFB'), + '2.16.840.1.101.3.4.1.25': ('id-aes192-wrap', ), + '2.16.840.1.101.3.4.1.26': ('aes-192-gcm', 'id-aes192-GCM'), + '2.16.840.1.101.3.4.1.27': ('aes-192-ccm', 'id-aes192-CCM'), + '2.16.840.1.101.3.4.1.28': ('id-aes192-wrap-pad', ), + '2.16.840.1.101.3.4.1.41': ('aes-256-ecb', 'AES-256-ECB'), + '2.16.840.1.101.3.4.1.42': ('aes-256-cbc', 'AES-256-CBC'), + '2.16.840.1.101.3.4.1.43': ('aes-256-ofb', 'AES-256-OFB'), + '2.16.840.1.101.3.4.1.44': ('aes-256-cfb', 'AES-256-CFB'), + '2.16.840.1.101.3.4.1.45': ('id-aes256-wrap', ), + '2.16.840.1.101.3.4.1.46': ('aes-256-gcm', 'id-aes256-GCM'), + '2.16.840.1.101.3.4.1.47': ('aes-256-ccm', 'id-aes256-CCM'), + '2.16.840.1.101.3.4.1.48': ('id-aes256-wrap-pad', ), + '2.16.840.1.101.3.4.2': ('nist_hashalgs', ), + '2.16.840.1.101.3.4.2.1': ('sha256', 'SHA256'), + '2.16.840.1.101.3.4.2.2': ('sha384', 'SHA384'), + '2.16.840.1.101.3.4.2.3': ('sha512', 'SHA512'), + '2.16.840.1.101.3.4.2.4': ('sha224', 'SHA224'), + '2.16.840.1.101.3.4.2.5': ('sha512-224', 'SHA512-224'), + '2.16.840.1.101.3.4.2.6': ('sha512-256', 'SHA512-256'), + '2.16.840.1.101.3.4.2.7': ('sha3-224', 'SHA3-224'), + '2.16.840.1.101.3.4.2.8': ('sha3-256', 'SHA3-256'), + '2.16.840.1.101.3.4.2.9': ('sha3-384', 'SHA3-384'), + '2.16.840.1.101.3.4.2.10': ('sha3-512', 'SHA3-512'), + '2.16.840.1.101.3.4.2.11': ('shake128', 'SHAKE128'), + '2.16.840.1.101.3.4.2.12': ('shake256', 'SHAKE256'), + '2.16.840.1.101.3.4.2.13': ('hmac-sha3-224', 'id-hmacWithSHA3-224'), + '2.16.840.1.101.3.4.2.14': ('hmac-sha3-256', 'id-hmacWithSHA3-256'), + '2.16.840.1.101.3.4.2.15': ('hmac-sha3-384', 'id-hmacWithSHA3-384'), + '2.16.840.1.101.3.4.2.16': ('hmac-sha3-512', 'id-hmacWithSHA3-512'), + '2.16.840.1.101.3.4.3': ('dsa_with_sha2', 'sigAlgs'), + '2.16.840.1.101.3.4.3.1': ('dsa_with_SHA224', ), + '2.16.840.1.101.3.4.3.2': ('dsa_with_SHA256', ), + '2.16.840.1.101.3.4.3.3': ('dsa_with_SHA384', 'id-dsa-with-sha384'), + '2.16.840.1.101.3.4.3.4': ('dsa_with_SHA512', 'id-dsa-with-sha512'), + '2.16.840.1.101.3.4.3.5': ('dsa_with_SHA3-224', 'id-dsa-with-sha3-224'), + '2.16.840.1.101.3.4.3.6': ('dsa_with_SHA3-256', 'id-dsa-with-sha3-256'), + '2.16.840.1.101.3.4.3.7': ('dsa_with_SHA3-384', 'id-dsa-with-sha3-384'), + '2.16.840.1.101.3.4.3.8': ('dsa_with_SHA3-512', 'id-dsa-with-sha3-512'), + '2.16.840.1.101.3.4.3.9': ('ecdsa_with_SHA3-224', 'id-ecdsa-with-sha3-224'), + '2.16.840.1.101.3.4.3.10': ('ecdsa_with_SHA3-256', 'id-ecdsa-with-sha3-256'), + '2.16.840.1.101.3.4.3.11': ('ecdsa_with_SHA3-384', 'id-ecdsa-with-sha3-384'), + '2.16.840.1.101.3.4.3.12': ('ecdsa_with_SHA3-512', 'id-ecdsa-with-sha3-512'), + '2.16.840.1.101.3.4.3.13': ('RSA-SHA3-224', 'id-rsassa-pkcs1-v1_5-with-sha3-224'), + '2.16.840.1.101.3.4.3.14': ('RSA-SHA3-256', 'id-rsassa-pkcs1-v1_5-with-sha3-256'), + '2.16.840.1.101.3.4.3.15': ('RSA-SHA3-384', 'id-rsassa-pkcs1-v1_5-with-sha3-384'), + '2.16.840.1.101.3.4.3.16': ('RSA-SHA3-512', 'id-rsassa-pkcs1-v1_5-with-sha3-512'), + '2.16.840.1.113730': ('Netscape Communications Corp.', 'Netscape'), + '2.16.840.1.113730.1': ('Netscape Certificate Extension', 'nsCertExt'), + '2.16.840.1.113730.1.1': ('Netscape Cert Type', 'nsCertType'), + '2.16.840.1.113730.1.2': ('Netscape Base Url', 'nsBaseUrl'), + '2.16.840.1.113730.1.3': ('Netscape Revocation Url', 'nsRevocationUrl'), + '2.16.840.1.113730.1.4': ('Netscape CA Revocation Url', 'nsCaRevocationUrl'), + '2.16.840.1.113730.1.7': ('Netscape Renewal Url', 'nsRenewalUrl'), + '2.16.840.1.113730.1.8': ('Netscape CA Policy Url', 'nsCaPolicyUrl'), + '2.16.840.1.113730.1.12': ('Netscape SSL Server Name', 'nsSslServerName'), + '2.16.840.1.113730.1.13': ('Netscape Comment', 'nsComment'), + '2.16.840.1.113730.2': ('Netscape Data Type', 'nsDataType'), + '2.16.840.1.113730.2.5': ('Netscape Certificate Sequence', 'nsCertSequence'), + '2.16.840.1.113730.4.1': ('Netscape Server Gated Crypto', 'nsSGC'), + '2.23': ('International Organizations', 'international-organizations'), + '2.23.42': ('Secure Electronic Transactions', 'id-set'), + '2.23.42.0': ('content types', 'set-ctype'), + '2.23.42.0.0': ('setct-PANData', ), + '2.23.42.0.1': ('setct-PANToken', ), + '2.23.42.0.2': ('setct-PANOnly', ), + '2.23.42.0.3': ('setct-OIData', ), + '2.23.42.0.4': ('setct-PI', ), + '2.23.42.0.5': ('setct-PIData', ), + '2.23.42.0.6': ('setct-PIDataUnsigned', ), + '2.23.42.0.7': ('setct-HODInput', ), + '2.23.42.0.8': ('setct-AuthResBaggage', ), + '2.23.42.0.9': ('setct-AuthRevReqBaggage', ), + '2.23.42.0.10': ('setct-AuthRevResBaggage', ), + '2.23.42.0.11': ('setct-CapTokenSeq', ), + '2.23.42.0.12': ('setct-PInitResData', ), + '2.23.42.0.13': ('setct-PI-TBS', ), + '2.23.42.0.14': ('setct-PResData', ), + '2.23.42.0.16': ('setct-AuthReqTBS', ), + '2.23.42.0.17': ('setct-AuthResTBS', ), + '2.23.42.0.18': ('setct-AuthResTBSX', ), + '2.23.42.0.19': ('setct-AuthTokenTBS', ), + '2.23.42.0.20': ('setct-CapTokenData', ), + '2.23.42.0.21': ('setct-CapTokenTBS', ), + '2.23.42.0.22': ('setct-AcqCardCodeMsg', ), + '2.23.42.0.23': ('setct-AuthRevReqTBS', ), + '2.23.42.0.24': ('setct-AuthRevResData', ), + '2.23.42.0.25': ('setct-AuthRevResTBS', ), + '2.23.42.0.26': ('setct-CapReqTBS', ), + '2.23.42.0.27': ('setct-CapReqTBSX', ), + '2.23.42.0.28': ('setct-CapResData', ), + '2.23.42.0.29': ('setct-CapRevReqTBS', ), + '2.23.42.0.30': ('setct-CapRevReqTBSX', ), + '2.23.42.0.31': ('setct-CapRevResData', ), + '2.23.42.0.32': ('setct-CredReqTBS', ), + '2.23.42.0.33': ('setct-CredReqTBSX', ), + '2.23.42.0.34': ('setct-CredResData', ), + '2.23.42.0.35': ('setct-CredRevReqTBS', ), + '2.23.42.0.36': ('setct-CredRevReqTBSX', ), + '2.23.42.0.37': ('setct-CredRevResData', ), + '2.23.42.0.38': ('setct-PCertReqData', ), + '2.23.42.0.39': ('setct-PCertResTBS', ), + '2.23.42.0.40': ('setct-BatchAdminReqData', ), + '2.23.42.0.41': ('setct-BatchAdminResData', ), + '2.23.42.0.42': ('setct-CardCInitResTBS', ), + '2.23.42.0.43': ('setct-MeAqCInitResTBS', ), + '2.23.42.0.44': ('setct-RegFormResTBS', ), + '2.23.42.0.45': ('setct-CertReqData', ), + '2.23.42.0.46': ('setct-CertReqTBS', ), + '2.23.42.0.47': ('setct-CertResData', ), + '2.23.42.0.48': ('setct-CertInqReqTBS', ), + '2.23.42.0.49': ('setct-ErrorTBS', ), + '2.23.42.0.50': ('setct-PIDualSignedTBE', ), + '2.23.42.0.51': ('setct-PIUnsignedTBE', ), + '2.23.42.0.52': ('setct-AuthReqTBE', ), + '2.23.42.0.53': ('setct-AuthResTBE', ), + '2.23.42.0.54': ('setct-AuthResTBEX', ), + '2.23.42.0.55': ('setct-AuthTokenTBE', ), + '2.23.42.0.56': ('setct-CapTokenTBE', ), + '2.23.42.0.57': ('setct-CapTokenTBEX', ), + '2.23.42.0.58': ('setct-AcqCardCodeMsgTBE', ), + '2.23.42.0.59': ('setct-AuthRevReqTBE', ), + '2.23.42.0.60': ('setct-AuthRevResTBE', ), + '2.23.42.0.61': ('setct-AuthRevResTBEB', ), + '2.23.42.0.62': ('setct-CapReqTBE', ), + '2.23.42.0.63': ('setct-CapReqTBEX', ), + '2.23.42.0.64': ('setct-CapResTBE', ), + '2.23.42.0.65': ('setct-CapRevReqTBE', ), + '2.23.42.0.66': ('setct-CapRevReqTBEX', ), + '2.23.42.0.67': ('setct-CapRevResTBE', ), + '2.23.42.0.68': ('setct-CredReqTBE', ), + '2.23.42.0.69': ('setct-CredReqTBEX', ), + '2.23.42.0.70': ('setct-CredResTBE', ), + '2.23.42.0.71': ('setct-CredRevReqTBE', ), + '2.23.42.0.72': ('setct-CredRevReqTBEX', ), + '2.23.42.0.73': ('setct-CredRevResTBE', ), + '2.23.42.0.74': ('setct-BatchAdminReqTBE', ), + '2.23.42.0.75': ('setct-BatchAdminResTBE', ), + '2.23.42.0.76': ('setct-RegFormReqTBE', ), + '2.23.42.0.77': ('setct-CertReqTBE', ), + '2.23.42.0.78': ('setct-CertReqTBEX', ), + '2.23.42.0.79': ('setct-CertResTBE', ), + '2.23.42.0.80': ('setct-CRLNotificationTBS', ), + '2.23.42.0.81': ('setct-CRLNotificationResTBS', ), + '2.23.42.0.82': ('setct-BCIDistributionTBS', ), + '2.23.42.1': ('message extensions', 'set-msgExt'), + '2.23.42.1.1': ('generic cryptogram', 'setext-genCrypt'), + '2.23.42.1.3': ('merchant initiated auth', 'setext-miAuth'), + '2.23.42.1.4': ('setext-pinSecure', ), + '2.23.42.1.5': ('setext-pinAny', ), + '2.23.42.1.7': ('setext-track2', ), + '2.23.42.1.8': ('additional verification', 'setext-cv'), + '2.23.42.3': ('set-attr', ), + '2.23.42.3.0': ('setAttr-Cert', ), + '2.23.42.3.0.0': ('set-rootKeyThumb', ), + '2.23.42.3.0.1': ('set-addPolicy', ), + '2.23.42.3.1': ('payment gateway capabilities', 'setAttr-PGWYcap'), + '2.23.42.3.2': ('setAttr-TokenType', ), + '2.23.42.3.2.1': ('setAttr-Token-EMV', ), + '2.23.42.3.2.2': ('setAttr-Token-B0Prime', ), + '2.23.42.3.3': ('issuer capabilities', 'setAttr-IssCap'), + '2.23.42.3.3.3': ('setAttr-IssCap-CVM', ), + '2.23.42.3.3.3.1': ('generate cryptogram', 'setAttr-GenCryptgrm'), + '2.23.42.3.3.4': ('setAttr-IssCap-T2', ), + '2.23.42.3.3.4.1': ('encrypted track 2', 'setAttr-T2Enc'), + '2.23.42.3.3.4.2': ('cleartext track 2', 'setAttr-T2cleartxt'), + '2.23.42.3.3.5': ('setAttr-IssCap-Sig', ), + '2.23.42.3.3.5.1': ('ICC or token signature', 'setAttr-TokICCsig'), + '2.23.42.3.3.5.2': ('secure device signature', 'setAttr-SecDevSig'), + '2.23.42.5': ('set-policy', ), + '2.23.42.5.0': ('set-policy-root', ), + '2.23.42.7': ('certificate extensions', 'set-certExt'), + '2.23.42.7.0': ('setCext-hashedRoot', ), + '2.23.42.7.1': ('setCext-certType', ), + '2.23.42.7.2': ('setCext-merchData', ), + '2.23.42.7.3': ('setCext-cCertRequired', ), + '2.23.42.7.4': ('setCext-tunneling', ), + '2.23.42.7.5': ('setCext-setExt', ), + '2.23.42.7.6': ('setCext-setQualf', ), + '2.23.42.7.7': ('setCext-PGWYcapabilities', ), + '2.23.42.7.8': ('setCext-TokenIdentifier', ), + '2.23.42.7.9': ('setCext-Track2Data', ), + '2.23.42.7.10': ('setCext-TokenType', ), + '2.23.42.7.11': ('setCext-IssuerCapabilities', ), + '2.23.42.8': ('set-brand', ), + '2.23.42.8.1': ('set-brand-IATA-ATA', ), + '2.23.42.8.4': ('set-brand-Visa', ), + '2.23.42.8.5': ('set-brand-MasterCard', ), + '2.23.42.8.30': ('set-brand-Diners', ), + '2.23.42.8.34': ('set-brand-AmericanExpress', ), + '2.23.42.8.35': ('set-brand-JCB', ), + '2.23.42.8.6011': ('set-brand-Novus', ), + '2.23.43': ('wap', ), + '2.23.43.1': ('wap-wsg', ), + '2.23.43.1.4': ('wap-wsg-idm-ecid', ), + '2.23.43.1.4.1': ('wap-wsg-idm-ecid-wtls1', ), + '2.23.43.1.4.3': ('wap-wsg-idm-ecid-wtls3', ), + '2.23.43.1.4.4': ('wap-wsg-idm-ecid-wtls4', ), + '2.23.43.1.4.5': ('wap-wsg-idm-ecid-wtls5', ), + '2.23.43.1.4.6': ('wap-wsg-idm-ecid-wtls6', ), + '2.23.43.1.4.7': ('wap-wsg-idm-ecid-wtls7', ), + '2.23.43.1.4.8': ('wap-wsg-idm-ecid-wtls8', ), + '2.23.43.1.4.9': ('wap-wsg-idm-ecid-wtls9', ), + '2.23.43.1.4.10': ('wap-wsg-idm-ecid-wtls10', ), + '2.23.43.1.4.11': ('wap-wsg-idm-ecid-wtls11', ), + '2.23.43.1.4.12': ('wap-wsg-idm-ecid-wtls12', ), +} diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/basic.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/basic.py new file mode 100644 index 000000000..11c688d2c --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/basic.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org> +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +try: + import cryptography + from cryptography import x509 + + # Older versions of cryptography (< 2.1) do not have __hash__ functions for + # general name objects (DNSName, IPAddress, ...), while providing overloaded + # equality and string representation operations. This makes it impossible to + # use them in hash-based data structures such as set or dict. Since we are + # actually doing that in x509_certificate, and potentially in other code, + # we need to monkey-patch __hash__ for these classes to make sure our code + # works fine. + if LooseVersion(cryptography.__version__) < LooseVersion('2.1'): + # A very simply hash function which relies on the representation + # of an object to be implemented. This is the case since at least + # cryptography 1.0, see + # https://github.com/pyca/cryptography/commit/7a9abce4bff36c05d26d8d2680303a6f64a0e84f + def simple_hash(self): + return hash(repr(self)) + + # The hash functions for the following types were added for cryptography 2.1: + # https://github.com/pyca/cryptography/commit/fbfc36da2a4769045f2373b004ddf0aff906cf38 + x509.DNSName.__hash__ = simple_hash + x509.DirectoryName.__hash__ = simple_hash + x509.GeneralName.__hash__ = simple_hash + x509.IPAddress.__hash__ = simple_hash + x509.OtherName.__hash__ = simple_hash + x509.RegisteredID.__hash__ = simple_hash + + if LooseVersion(cryptography.__version__) < LooseVersion('1.2'): + # The hash functions for the following types were added for cryptography 1.2: + # https://github.com/pyca/cryptography/commit/b642deed88a8696e5f01ce6855ccf89985fc35d0 + # https://github.com/pyca/cryptography/commit/d1b5681f6db2bde7a14625538bd7907b08dfb486 + x509.RFC822Name.__hash__ = simple_hash + x509.UniformResourceIdentifier.__hash__ = simple_hash + + # Test whether we have support for DSA, EC, Ed25519, Ed448, RSA, X25519 and/or X448 + try: + # added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/dsa/ + import cryptography.hazmat.primitives.asymmetric.dsa + CRYPTOGRAPHY_HAS_DSA = True + try: + # added later in 1.5 + cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey.sign + CRYPTOGRAPHY_HAS_DSA_SIGN = True + except AttributeError: + CRYPTOGRAPHY_HAS_DSA_SIGN = False + except ImportError: + CRYPTOGRAPHY_HAS_DSA = False + CRYPTOGRAPHY_HAS_DSA_SIGN = False + try: + # added in 2.6 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed25519/ + import cryptography.hazmat.primitives.asymmetric.ed25519 + CRYPTOGRAPHY_HAS_ED25519 = True + try: + # added with the primitive in 2.6 + cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.sign + CRYPTOGRAPHY_HAS_ED25519_SIGN = True + except AttributeError: + CRYPTOGRAPHY_HAS_ED25519_SIGN = False + except ImportError: + CRYPTOGRAPHY_HAS_ED25519 = False + CRYPTOGRAPHY_HAS_ED25519_SIGN = False + try: + # added in 2.6 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed448/ + import cryptography.hazmat.primitives.asymmetric.ed448 + CRYPTOGRAPHY_HAS_ED448 = True + try: + # added with the primitive in 2.6 + cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.sign + CRYPTOGRAPHY_HAS_ED448_SIGN = True + except AttributeError: + CRYPTOGRAPHY_HAS_ED448_SIGN = False + except ImportError: + CRYPTOGRAPHY_HAS_ED448 = False + CRYPTOGRAPHY_HAS_ED448_SIGN = False + try: + # added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/ + import cryptography.hazmat.primitives.asymmetric.ec + CRYPTOGRAPHY_HAS_EC = True + try: + # added later in 1.5 + cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey.sign + CRYPTOGRAPHY_HAS_EC_SIGN = True + except AttributeError: + CRYPTOGRAPHY_HAS_EC_SIGN = False + except ImportError: + CRYPTOGRAPHY_HAS_EC = False + CRYPTOGRAPHY_HAS_EC_SIGN = False + try: + # added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/ + import cryptography.hazmat.primitives.asymmetric.rsa + CRYPTOGRAPHY_HAS_RSA = True + try: + # added later in 1.4 + cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey.sign + CRYPTOGRAPHY_HAS_RSA_SIGN = True + except AttributeError: + CRYPTOGRAPHY_HAS_RSA_SIGN = False + except ImportError: + CRYPTOGRAPHY_HAS_RSA = False + CRYPTOGRAPHY_HAS_RSA_SIGN = False + try: + # added in 2.0 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x25519/ + import cryptography.hazmat.primitives.asymmetric.x25519 + CRYPTOGRAPHY_HAS_X25519 = True + try: + # added later in 2.5 + cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.private_bytes + CRYPTOGRAPHY_HAS_X25519_FULL = True + except AttributeError: + CRYPTOGRAPHY_HAS_X25519_FULL = False + except ImportError: + CRYPTOGRAPHY_HAS_X25519 = False + CRYPTOGRAPHY_HAS_X25519_FULL = False + try: + # added in 2.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x448/ + import cryptography.hazmat.primitives.asymmetric.x448 + CRYPTOGRAPHY_HAS_X448 = True + except ImportError: + CRYPTOGRAPHY_HAS_X448 = False + + HAS_CRYPTOGRAPHY = True +except ImportError: + # Error handled in the calling module. + CRYPTOGRAPHY_HAS_EC = False + CRYPTOGRAPHY_HAS_EC_SIGN = False + CRYPTOGRAPHY_HAS_ED25519 = False + CRYPTOGRAPHY_HAS_ED25519_SIGN = False + CRYPTOGRAPHY_HAS_ED448 = False + CRYPTOGRAPHY_HAS_ED448_SIGN = False + CRYPTOGRAPHY_HAS_DSA = False + CRYPTOGRAPHY_HAS_DSA_SIGN = False + CRYPTOGRAPHY_HAS_RSA = False + CRYPTOGRAPHY_HAS_RSA_SIGN = False + CRYPTOGRAPHY_HAS_X25519 = False + CRYPTOGRAPHY_HAS_X25519_FULL = False + CRYPTOGRAPHY_HAS_X448 = False + HAS_CRYPTOGRAPHY = False + + +class OpenSSLObjectError(Exception): + pass + + +class OpenSSLBadPassphraseError(OpenSSLObjectError): + pass diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_crl.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_crl.py new file mode 100644 index 000000000..62499e08b --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_crl.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +try: + from cryptography import x509 +except ImportError: + # Error handled in the calling module. + pass + +from .basic import ( + HAS_CRYPTOGRAPHY, +) + +from .cryptography_support import ( + cryptography_decode_name, +) + +from ._obj2txt import ( + obj2txt, +) + + +TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" + + +if HAS_CRYPTOGRAPHY: + REVOCATION_REASON_MAP = { + 'unspecified': x509.ReasonFlags.unspecified, + 'key_compromise': x509.ReasonFlags.key_compromise, + 'ca_compromise': x509.ReasonFlags.ca_compromise, + 'affiliation_changed': x509.ReasonFlags.affiliation_changed, + 'superseded': x509.ReasonFlags.superseded, + 'cessation_of_operation': x509.ReasonFlags.cessation_of_operation, + 'certificate_hold': x509.ReasonFlags.certificate_hold, + 'privilege_withdrawn': x509.ReasonFlags.privilege_withdrawn, + 'aa_compromise': x509.ReasonFlags.aa_compromise, + 'remove_from_crl': x509.ReasonFlags.remove_from_crl, + } + REVOCATION_REASON_MAP_INVERSE = dict() + for k, v in REVOCATION_REASON_MAP.items(): + REVOCATION_REASON_MAP_INVERSE[v] = k + +else: + REVOCATION_REASON_MAP = dict() + REVOCATION_REASON_MAP_INVERSE = dict() + + +def cryptography_decode_revoked_certificate(cert): + result = { + 'serial_number': cert.serial_number, + 'revocation_date': cert.revocation_date, + 'issuer': None, + 'issuer_critical': False, + 'reason': None, + 'reason_critical': False, + 'invalidity_date': None, + 'invalidity_date_critical': False, + } + try: + ext = cert.extensions.get_extension_for_class(x509.CertificateIssuer) + result['issuer'] = list(ext.value) + result['issuer_critical'] = ext.critical + except x509.ExtensionNotFound: + pass + try: + ext = cert.extensions.get_extension_for_class(x509.CRLReason) + result['reason'] = ext.value.reason + result['reason_critical'] = ext.critical + except x509.ExtensionNotFound: + pass + try: + ext = cert.extensions.get_extension_for_class(x509.InvalidityDate) + result['invalidity_date'] = ext.value.invalidity_date + result['invalidity_date_critical'] = ext.critical + except x509.ExtensionNotFound: + pass + return result + + +def cryptography_dump_revoked(entry, idn_rewrite='ignore'): + return { + 'serial_number': entry['serial_number'], + 'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT), + 'issuer': + [cryptography_decode_name(issuer, idn_rewrite=idn_rewrite) for issuer in entry['issuer']] + if entry['issuer'] is not None else None, + 'issuer_critical': entry['issuer_critical'], + 'reason': 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 cryptography_get_signature_algorithm_oid_from_crl(crl): + try: + return crl.signature_algorithm_oid + except AttributeError: + # Older cryptography versions do not have signature_algorithm_oid yet + dotted = obj2txt( + crl._backend._lib, + crl._backend._ffi, + crl._x509_crl.sig_alg.algorithm + ) + return x509.oid.ObjectIdentifier(dotted) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_support.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_support.py new file mode 100644 index 000000000..fde691997 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_support.py @@ -0,0 +1,809 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import base64 +import binascii +import re +import sys +import traceback + +from ansible.module_utils.common.text.converters import to_text, to_bytes, to_native +from ansible.module_utils.six.moves.urllib.parse import urlparse, urlunparse, ParseResult + +from ._asn1 import serialize_asn1_string_as_der + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +try: + import cryptography + from cryptography import x509 + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import padding + import ipaddress +except ImportError: + # Error handled in the calling module. + pass + +try: + import cryptography.hazmat.primitives.asymmetric.rsa +except ImportError: + pass +try: + import cryptography.hazmat.primitives.asymmetric.ec +except ImportError: + pass +try: + import cryptography.hazmat.primitives.asymmetric.dsa +except ImportError: + pass +try: + import cryptography.hazmat.primitives.asymmetric.ed25519 +except ImportError: + pass +try: + import cryptography.hazmat.primitives.asymmetric.ed448 +except ImportError: + pass + +try: + # This is a separate try/except since this is only present in cryptography 2.5 or newer + from cryptography.hazmat.primitives.serialization.pkcs12 import ( + load_key_and_certificates as _load_key_and_certificates, + ) +except ImportError: + # Error handled in the calling module. + _load_key_and_certificates = None + +try: + # This is a separate try/except since this is only present in cryptography 36.0.0 or newer + from cryptography.hazmat.primitives.serialization.pkcs12 import ( + load_pkcs12 as _load_pkcs12, + ) +except ImportError: + # Error handled in the calling module. + _load_pkcs12 = None + +try: + import idna + + HAS_IDNA = True +except ImportError: + HAS_IDNA = False + IDNA_IMP_ERROR = traceback.format_exc() + +from ansible.module_utils.basic import missing_required_lib + +from .basic import ( + CRYPTOGRAPHY_HAS_DSA_SIGN, + CRYPTOGRAPHY_HAS_EC_SIGN, + CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED25519_SIGN, + CRYPTOGRAPHY_HAS_ED448, + CRYPTOGRAPHY_HAS_ED448_SIGN, + CRYPTOGRAPHY_HAS_RSA_SIGN, + CRYPTOGRAPHY_HAS_X25519, + CRYPTOGRAPHY_HAS_X25519_FULL, + CRYPTOGRAPHY_HAS_X448, + OpenSSLObjectError, +) + +from ._objects import ( + OID_LOOKUP, + OID_MAP, + NORMALIZE_NAMES_SHORT, + NORMALIZE_NAMES, +) + +from ._obj2txt import obj2txt + + +DOTTED_OID = re.compile(r'^\d+(?:\.\d+)+$') + + +def cryptography_get_extensions_from_cert(cert): + result = dict() + try: + # Since cryptography will not give us the DER value for an extension + # (that is only stored for unrecognized extensions), we have to re-do + # the extension parsing outselves. + backend = default_backend() + try: + # For certain old versions of cryptography, backend is a MultiBackend object, + # which has no _lib attribute. In that case, revert to the old approach. + backend._lib + except AttributeError: + backend = cert._backend + + x509_obj = cert._x509 + # With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does + # not allow to get the raw value of an extension, so we have to use this ugly hack: + exts = list(cert.extensions) + + for i in range(backend._lib.X509_get_ext_count(x509_obj)): + ext = backend._lib.X509_get_ext(x509_obj, i) + if ext == backend._ffi.NULL: + continue + crit = backend._lib.X509_EXTENSION_get_critical(ext) + data = backend._lib.X509_EXTENSION_get_data(ext) + backend.openssl_assert(data != backend._ffi.NULL) + der = backend._ffi.buffer(data.data, data.length)[:] + entry = dict( + critical=(crit == 1), + value=to_native(base64.b64encode(der)), + ) + try: + oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext)) + except AttributeError: + oid = exts[i].oid.dotted_string + result[oid] = entry + + except Exception: + # In case the above method breaks, we likely have cryptography 36.0.0 or newer. + # Use it's public_bytes() feature in that case. We will later switch this around + # so that this code will be the default, but for now this will act as a fallback + # since it will re-serialize de-serialized data, which can be different (if the + # original data was not canonicalized) from what was contained in the certificate. + for ext in cert.extensions: + result[ext.oid.dotted_string] = dict( + critical=ext.critical, + value=to_native(base64.b64encode(ext.value.public_bytes())), + ) + + return result + + +def cryptography_get_extensions_from_csr(csr): + result = dict() + try: + # Since cryptography will not give us the DER value for an extension + # (that is only stored for unrecognized extensions), we have to re-do + # the extension parsing outselves. + backend = default_backend() + try: + # For certain old versions of cryptography, backend is a MultiBackend object, + # which has no _lib attribute. In that case, revert to the old approach. + backend._lib + except AttributeError: + backend = csr._backend + + extensions = backend._lib.X509_REQ_get_extensions(csr._x509_req) + extensions = backend._ffi.gc( + extensions, + lambda ext: backend._lib.sk_X509_EXTENSION_pop_free( + ext, + backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free") + ) + ) + + # With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does + # not allow to get the raw value of an extension, so we have to use this ugly hack: + exts = list(csr.extensions) + + for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)): + ext = backend._lib.sk_X509_EXTENSION_value(extensions, i) + if ext == backend._ffi.NULL: + continue + crit = backend._lib.X509_EXTENSION_get_critical(ext) + data = backend._lib.X509_EXTENSION_get_data(ext) + backend.openssl_assert(data != backend._ffi.NULL) + der = backend._ffi.buffer(data.data, data.length)[:] + entry = dict( + critical=(crit == 1), + value=to_native(base64.b64encode(der)), + ) + try: + oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext)) + except AttributeError: + oid = exts[i].oid.dotted_string + result[oid] = entry + + except Exception: + # In case the above method breaks, we likely have cryptography 36.0.0 or newer. + # Use it's public_bytes() feature in that case. We will later switch this around + # so that this code will be the default, but for now this will act as a fallback + # since it will re-serialize de-serialized data, which can be different (if the + # original data was not canonicalized) from what was contained in the CSR. + for ext in csr.extensions: + result[ext.oid.dotted_string] = dict( + critical=ext.critical, + value=to_native(base64.b64encode(ext.value.public_bytes())), + ) + + return result + + +def cryptography_name_to_oid(name): + dotted = OID_LOOKUP.get(name) + if dotted is None: + if DOTTED_OID.match(name): + return x509.oid.ObjectIdentifier(name) + raise OpenSSLObjectError('Cannot find OID for "{0}"'.format(name)) + return x509.oid.ObjectIdentifier(dotted) + + +def cryptography_oid_to_name(oid, short=False): + dotted_string = oid.dotted_string + names = OID_MAP.get(dotted_string) + if names: + name = names[0] + else: + name = oid._name + if name == 'Unknown OID': + name = dotted_string + if short: + return NORMALIZE_NAMES_SHORT.get(name, name) + else: + return NORMALIZE_NAMES.get(name, name) + + +def _get_hex(bytesstr): + if bytesstr is None: + return bytesstr + data = binascii.hexlify(bytesstr) + data = to_text(b':'.join(data[i:i + 2] for i in range(0, len(data), 2))) + return data + + +def _parse_hex(bytesstr): + if bytesstr is None: + return bytesstr + data = ''.join([('0' * (2 - len(p)) + p) if len(p) < 2 else p for p in to_text(bytesstr).split(':')]) + data = binascii.unhexlify(data) + return data + + +DN_COMPONENT_START_RE = re.compile(b'^ *([a-zA-z0-9.]+) *= *') +DN_HEX_LETTER = b'0123456789abcdef' + + +if sys.version_info[0] < 3: + _int_to_byte = chr +else: + def _int_to_byte(value): + return bytes((value, )) + + +def _parse_dn_component(name, sep=b',', decode_remainder=True): + m = DN_COMPONENT_START_RE.match(name) + if not m: + raise OpenSSLObjectError(u'cannot start part in "{0}"'.format(to_text(name))) + oid = cryptography_name_to_oid(to_text(m.group(1))) + idx = len(m.group(0)) + decoded_name = [] + sep_str = sep + b'\\' + if decode_remainder: + length = len(name) + if length > idx and name[idx:idx + 1] == b'#': + # Decoding a hex string + idx += 1 + while idx + 1 < length: + ch1 = name[idx:idx + 1] + ch2 = name[idx + 1:idx + 2] + idx1 = DN_HEX_LETTER.find(ch1.lower()) + idx2 = DN_HEX_LETTER.find(ch2.lower()) + if idx1 < 0 or idx2 < 0: + raise OpenSSLObjectError(u'Invalid hex sequence entry "{0}"'.format(to_text(ch1 + ch2))) + idx += 2 + decoded_name.append(_int_to_byte(idx1 * 16 + idx2)) + else: + # Decoding a regular string + while idx < length: + i = idx + while i < length and name[i:i + 1] not in sep_str: + i += 1 + if i > idx: + decoded_name.append(name[idx:i]) + idx = i + while idx + 1 < length and name[idx:idx + 1] == b'\\': + ch = name[idx + 1:idx + 2] + idx1 = DN_HEX_LETTER.find(ch.lower()) + if idx1 >= 0: + if idx + 2 >= length: + raise OpenSSLObjectError(u'Hex escape sequence "\\{0}" incomplete at end of string'.format(to_text(ch))) + ch2 = name[idx + 2:idx + 3] + idx2 = DN_HEX_LETTER.find(ch2.lower()) + if idx2 < 0: + raise OpenSSLObjectError(u'Hex escape sequence "\\{0}" has invalid second letter'.format(to_text(ch + ch2))) + ch = _int_to_byte(idx1 * 16 + idx2) + idx += 1 + idx += 2 + decoded_name.append(ch) + if idx < length and name[idx:idx + 1] == sep: + break + else: + decoded_name.append(name[idx:]) + idx = len(name) + return x509.NameAttribute(oid, to_text(b''.join(decoded_name))), name[idx:] + + +def _parse_dn(name): + ''' + Parse a Distinguished Name. + + Can be of the form ``CN=Test, O = Something`` or ``CN = Test,O= Something``. + ''' + original_name = name + name = name.lstrip() + sep = b',' + if name.startswith(b'/'): + sep = b'/' + name = name[1:] + result = [] + while name: + try: + attribute, name = _parse_dn_component(name, sep=sep) + except OpenSSLObjectError as e: + raise OpenSSLObjectError(u'Error while parsing distinguished name "{0}": {1}'.format(to_text(original_name), e)) + result.append(attribute) + if name: + if name[0:1] != sep or len(name) < 2: + raise OpenSSLObjectError(u'Error while parsing distinguished name "{0}": unexpected end of string'.format(to_text(original_name))) + name = name[1:] + return result + + +def cryptography_parse_relative_distinguished_name(rdn): + names = [] + for part in rdn: + try: + names.append(_parse_dn_component(to_bytes(part), decode_remainder=False)[0]) + except OpenSSLObjectError as e: + raise OpenSSLObjectError(u'Error while parsing relative distinguished name "{0}": {1}'.format(part, e)) + return cryptography.x509.RelativeDistinguishedName(names) + + +def _is_ascii(value): + '''Check whether the Unicode string `value` contains only ASCII characters.''' + try: + value.encode("ascii") + return True + except UnicodeEncodeError: + return False + + +def _adjust_idn(value, idn_rewrite): + if idn_rewrite == 'ignore' or not value: + return value + if idn_rewrite == 'idna' and _is_ascii(value): + return value + if idn_rewrite not in ('idna', 'unicode'): + raise ValueError('Invalid value for idn_rewrite: "{0}"'.format(idn_rewrite)) + if not HAS_IDNA: + raise OpenSSLObjectError( + missing_required_lib('idna', reason='to transform {what} DNS name "{name}" to {dest}'.format( + name=value, + what='IDNA' if idn_rewrite == 'unicode' else 'Unicode', + dest='Unicode' if idn_rewrite == 'unicode' else 'IDNA', + ))) + # Since IDNA does not like '*' or empty labels (except one empty label at the end), + # we split and let IDNA only handle labels that are neither empty or '*'. + parts = value.split(u'.') + for index, part in enumerate(parts): + if part in (u'', u'*'): + continue + try: + if idn_rewrite == 'idna': + parts[index] = idna.encode(part).decode('ascii') + elif idn_rewrite == 'unicode' and part.startswith(u'xn--'): + parts[index] = idna.decode(part) + except idna.IDNAError as exc2008: + try: + if idn_rewrite == 'idna': + parts[index] = part.encode('idna').decode('ascii') + elif idn_rewrite == 'unicode' and part.startswith(u'xn--'): + parts[index] = part.encode('ascii').decode('idna') + except Exception as exc2003: + raise OpenSSLObjectError( + u'Error while transforming part "{part}" of {what} DNS name "{name}" to {dest}.' + u' IDNA2008 transformation resulted in "{exc2008}", IDNA2003 transformation resulted in "{exc2003}".'.format( + part=part, + name=value, + what='IDNA' if idn_rewrite == 'unicode' else 'Unicode', + dest='Unicode' if idn_rewrite == 'unicode' else 'IDNA', + exc2003=exc2003, + exc2008=exc2008, + )) + return u'.'.join(parts) + + +def _adjust_idn_email(value, idn_rewrite): + idx = value.find(u'@') + if idx < 0: + return value + return u'{0}@{1}'.format(value[:idx], _adjust_idn(value[idx + 1:], idn_rewrite)) + + +def _adjust_idn_url(value, idn_rewrite): + url = urlparse(value) + host = _adjust_idn(url.hostname, idn_rewrite) + if url.username is not None and url.password is not None: + host = u'{0}:{1}@{2}'.format(url.username, url.password, host) + elif url.username is not None: + host = u'{0}@{1}'.format(url.username, host) + if url.port is not None: + host = u'{0}:{1}'.format(host, url.port) + return urlunparse( + ParseResult(scheme=url.scheme, netloc=host, path=url.path, params=url.params, query=url.query, fragment=url.fragment)) + + +def cryptography_get_name(name, what='Subject Alternative Name'): + ''' + Given a name string, returns a cryptography x509.GeneralName object. + Raises an OpenSSLObjectError if the name is unknown or cannot be parsed. + ''' + try: + if name.startswith('DNS:'): + return x509.DNSName(_adjust_idn(to_text(name[4:]), 'idna')) + if name.startswith('IP:'): + address = to_text(name[3:]) + if '/' in address: + return x509.IPAddress(ipaddress.ip_network(address)) + return x509.IPAddress(ipaddress.ip_address(address)) + if name.startswith('email:'): + return x509.RFC822Name(_adjust_idn_email(to_text(name[6:]), 'idna')) + if name.startswith('URI:'): + return x509.UniformResourceIdentifier(_adjust_idn_url(to_text(name[4:]), 'idna')) + if name.startswith('RID:'): + m = re.match(r'^([0-9]+(?:\.[0-9]+)*)$', to_text(name[4:])) + if not m: + raise OpenSSLObjectError('Cannot parse {what} "{name}"'.format(name=name, what=what)) + return x509.RegisteredID(x509.oid.ObjectIdentifier(m.group(1))) + if name.startswith('otherName:'): + # otherName can either be a raw ASN.1 hex string or in the format that OpenSSL works with. + m = re.match(r'^([0-9]+(?:\.[0-9]+)*);([0-9a-fA-F]{1,2}(?::[0-9a-fA-F]{1,2})*)$', to_text(name[10:])) + if m: + return x509.OtherName(x509.oid.ObjectIdentifier(m.group(1)), _parse_hex(m.group(2))) + + # See https://www.openssl.org/docs/man1.0.2/man5/x509v3_config.html - Subject Alternative Name for more + # defailts on the format expected. + name = to_text(name[10:], errors='surrogate_or_strict') + if ';' not in name: + raise OpenSSLObjectError('Cannot parse {what} otherName "{name}", must be in the ' + 'format "otherName:<OID>;<ASN.1 OpenSSL Encoded String>" or ' + '"otherName:<OID>;<hex string>"'.format(name=name, what=what)) + + oid, value = name.split(';', 1) + b_value = serialize_asn1_string_as_der(value) + return x509.OtherName(x509.ObjectIdentifier(oid), b_value) + if name.startswith('dirName:'): + return x509.DirectoryName(x509.Name(reversed(_parse_dn(to_bytes(name[8:]))))) + except Exception as e: + raise OpenSSLObjectError('Cannot parse {what} "{name}": {error}'.format(name=name, what=what, error=e)) + if ':' not in name: + raise OpenSSLObjectError('Cannot parse {what} "{name}" (forgot "DNS:" prefix?)'.format(name=name, what=what)) + raise OpenSSLObjectError('Cannot parse {what} "{name}" (potentially unsupported by cryptography backend)'.format(name=name, what=what)) + + +def _dn_escape_value(value): + ''' + Escape Distinguished Name's attribute value. + ''' + value = value.replace(u'\\', u'\\\\') + for ch in [u',', u'+', u'<', u'>', u';', u'"']: + value = value.replace(ch, u'\\%s' % ch) + value = value.replace(u'\0', u'\\00') + if value.startswith((u' ', u'#')): + value = u'\\%s' % value[0] + value[1:] + if value.endswith(u' '): + value = value[:-1] + u'\\ ' + return value + + +def cryptography_decode_name(name, idn_rewrite='ignore'): + ''' + Given a cryptography x509.GeneralName object, returns a string. + Raises an OpenSSLObjectError if the name is not supported. + ''' + if idn_rewrite not in ('ignore', 'idna', 'unicode'): + raise AssertionError('idn_rewrite must be one of "ignore", "idna", or "unicode"') + if isinstance(name, x509.DNSName): + return u'DNS:{0}'.format(_adjust_idn(name.value, idn_rewrite)) + if isinstance(name, x509.IPAddress): + if isinstance(name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network)): + return u'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen) + return u'IP:{0}'.format(name.value.compressed) + if isinstance(name, x509.RFC822Name): + return u'email:{0}'.format(_adjust_idn_email(name.value, idn_rewrite)) + if isinstance(name, x509.UniformResourceIdentifier): + return u'URI:{0}'.format(_adjust_idn_url(name.value, idn_rewrite)) + if isinstance(name, x509.DirectoryName): + # According to https://datatracker.ietf.org/doc/html/rfc4514.html#section-2.1 the + # list needs to be reversed, and joined by commas + return u'dirName:' + ','.join([ + u'{0}={1}'.format(to_text(cryptography_oid_to_name(attribute.oid, short=True)), _dn_escape_value(attribute.value)) + for attribute in reversed(list(name.value)) + ]) + if isinstance(name, x509.RegisteredID): + return u'RID:{0}'.format(name.value.dotted_string) + if isinstance(name, x509.OtherName): + return u'otherName:{0};{1}'.format(name.type_id.dotted_string, _get_hex(name.value)) + raise OpenSSLObjectError('Cannot decode name "{0}"'.format(name)) + + +def _cryptography_get_keyusage(usage): + ''' + Given a key usage identifier string, returns the parameter name used by cryptography's x509.KeyUsage(). + Raises an OpenSSLObjectError if the identifier is unknown. + ''' + if usage in ('Digital Signature', 'digitalSignature'): + return 'digital_signature' + if usage in ('Non Repudiation', 'nonRepudiation'): + return 'content_commitment' + if usage in ('Key Encipherment', 'keyEncipherment'): + return 'key_encipherment' + if usage in ('Data Encipherment', 'dataEncipherment'): + return 'data_encipherment' + if usage in ('Key Agreement', 'keyAgreement'): + return 'key_agreement' + if usage in ('Certificate Sign', 'keyCertSign'): + return 'key_cert_sign' + if usage in ('CRL Sign', 'cRLSign'): + return 'crl_sign' + if usage in ('Encipher Only', 'encipherOnly'): + return 'encipher_only' + if usage in ('Decipher Only', 'decipherOnly'): + return 'decipher_only' + raise OpenSSLObjectError('Unknown key usage "{0}"'.format(usage)) + + +def cryptography_parse_key_usage_params(usages): + ''' + Given a list of key usage identifier strings, returns the parameters for cryptography's x509.KeyUsage(). + Raises an OpenSSLObjectError if an identifier is unknown. + ''' + params = dict( + digital_signature=False, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ) + for usage in usages: + params[_cryptography_get_keyusage(usage)] = True + return params + + +def cryptography_get_basic_constraints(constraints): + ''' + Given a list of constraints, returns a tuple (ca, path_length). + Raises an OpenSSLObjectError if a constraint is unknown or cannot be parsed. + ''' + ca = False + path_length = None + if constraints: + for constraint in constraints: + if constraint.startswith('CA:'): + if constraint == 'CA:TRUE': + ca = True + elif constraint == 'CA:FALSE': + ca = False + else: + raise OpenSSLObjectError('Unknown basic constraint value "{0}" for CA'.format(constraint[3:])) + elif constraint.startswith('pathlen:'): + v = constraint[len('pathlen:'):] + try: + path_length = int(v) + except Exception as e: + raise OpenSSLObjectError('Cannot parse path length constraint "{0}" ({1})'.format(v, e)) + else: + raise OpenSSLObjectError('Unknown basic constraint "{0}"'.format(constraint)) + return ca, path_length + + +def cryptography_key_needs_digest_for_signing(key): + '''Tests whether the given private key requires a digest algorithm for signing. + + Ed25519 and Ed448 keys do not; they need None to be passed as the digest algorithm. + ''' + if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): + return False + if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): + return False + return True + + +def _compare_public_keys(key1, key2, clazz): + a = isinstance(key1, clazz) + b = isinstance(key2, clazz) + if not (a or b): + return None + if not a or not b: + return False + a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + return a == b + + +def cryptography_compare_public_keys(key1, key2): + '''Tests whether two public keys are the same. + + Needs special logic for Ed25519 and Ed448 keys, since they do not have public_numbers(). + ''' + if CRYPTOGRAPHY_HAS_ED25519: + res = _compare_public_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey) + if res is not None: + return res + if CRYPTOGRAPHY_HAS_ED448: + res = _compare_public_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey) + if res is not None: + return res + return key1.public_numbers() == key2.public_numbers() + + +def _compare_private_keys(key1, key2, clazz, has_no_private_bytes=False): + a = isinstance(key1, clazz) + b = isinstance(key2, clazz) + if not (a or b): + return None + if not a or not b: + return False + if has_no_private_bytes: + # We do not have the private_bytes() function - compare associated public keys + return cryptography_compare_public_keys(a.public_key(), b.public_key()) + encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption() + a = key1.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, encryption_algorithm=encryption_algorithm) + b = key2.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, encryption_algorithm=encryption_algorithm) + return a == b + + +def cryptography_compare_private_keys(key1, key2): + '''Tests whether two private keys are the same. + + Needs special logic for Ed25519, X25519, and Ed448 keys, since they do not have private_numbers(). + ''' + if CRYPTOGRAPHY_HAS_ED25519: + res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey) + if res is not None: + return res + if CRYPTOGRAPHY_HAS_X25519: + res = _compare_private_keys( + key1, key2, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey, has_no_private_bytes=not CRYPTOGRAPHY_HAS_X25519_FULL) + if res is not None: + return res + if CRYPTOGRAPHY_HAS_ED448: + res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey) + if res is not None: + return res + if CRYPTOGRAPHY_HAS_X448: + res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey) + if res is not None: + return res + return key1.private_numbers() == key2.private_numbers() + + +def cryptography_serial_number_of_cert(cert): + '''Returns cert.serial_number. + + Also works for old versions of cryptography. + ''' + try: + return cert.serial_number + except AttributeError: + # The property was called "serial" before cryptography 1.4 + return cert.serial + + +def parse_pkcs12(pkcs12_bytes, passphrase=None): + '''Returns a tuple (private_key, certificate, additional_certificates, friendly_name). + ''' + if _load_pkcs12 is None and _load_key_and_certificates is None: + raise ValueError('neither load_pkcs12() nor load_key_and_certificates() present in the current cryptography version') + + if passphrase is not None: + passphrase = to_bytes(passphrase) + + # Main code for cryptography 36.0.0 and forward + if _load_pkcs12 is not None: + return _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase) + + if LooseVersion(cryptography.__version__) >= LooseVersion('35.0'): + return _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase) + + return _parse_pkcs12_legacy(pkcs12_bytes, passphrase) + + +def _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase=None): + # Requires cryptography 36.0.0 or newer + pkcs12 = _load_pkcs12(pkcs12_bytes, passphrase) + additional_certificates = [cert.certificate for cert in pkcs12.additional_certs] + private_key = pkcs12.key + certificate = None + friendly_name = None + if pkcs12.cert: + certificate = pkcs12.cert.certificate + friendly_name = pkcs12.cert.friendly_name + return private_key, certificate, additional_certificates, friendly_name + + +def _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase=None): + # Backwards compatibility code for cryptography 35.x + private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase) + + friendly_name = None + if certificate: + # See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238 + backend = default_backend() + + # This code basically does what load_key_and_certificates() does, but without error-checking. + # Since load_key_and_certificates succeeded, it should not fail. + pkcs12 = backend._ffi.gc( + backend._lib.d2i_PKCS12_bio(backend._bytes_to_bio(pkcs12_bytes).bio, backend._ffi.NULL), + backend._lib.PKCS12_free) + certificate_x509_ptr = backend._ffi.new("X509 **") + with backend._zeroed_null_terminated_buf(to_bytes(passphrase) if passphrase is not None else None) as passphrase_buffer: + backend._lib.PKCS12_parse( + pkcs12, + passphrase_buffer, + backend._ffi.new("EVP_PKEY **"), + certificate_x509_ptr, + backend._ffi.new("Cryptography_STACK_OF_X509 **")) + if certificate_x509_ptr[0] != backend._ffi.NULL: + maybe_name = backend._lib.X509_alias_get0(certificate_x509_ptr[0], backend._ffi.NULL) + if maybe_name != backend._ffi.NULL: + friendly_name = backend._ffi.string(maybe_name) + + return private_key, certificate, additional_certificates, friendly_name + + +def _parse_pkcs12_legacy(pkcs12_bytes, passphrase=None): + # Backwards compatibility code for cryptography < 35.0.0 + private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase) + + friendly_name = None + if certificate: + # See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238 + backend = certificate._backend + maybe_name = backend._lib.X509_alias_get0(certificate._x509, backend._ffi.NULL) + if maybe_name != backend._ffi.NULL: + friendly_name = backend._ffi.string(maybe_name) + return private_key, certificate, additional_certificates, friendly_name + + +def cryptography_verify_signature(signature, data, hash_algorithm, signer_public_key): + ''' + Check whether the given signature of the given data was signed by the given public key object. + ''' + try: + if CRYPTOGRAPHY_HAS_RSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey): + signer_public_key.verify(signature, data, padding.PKCS1v15(), hash_algorithm) + return True + if CRYPTOGRAPHY_HAS_EC_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey): + signer_public_key.verify(signature, data, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hash_algorithm)) + return True + if CRYPTOGRAPHY_HAS_DSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey): + signer_public_key.verify(signature, data, hash_algorithm) + return True + if CRYPTOGRAPHY_HAS_ED25519_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey): + signer_public_key.verify(signature, data) + return True + if CRYPTOGRAPHY_HAS_ED448_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey): + signer_public_key.verify(signature, data) + return True + raise OpenSSLObjectError(u'Unsupported public key type {0}'.format(type(signer_public_key))) + except InvalidSignature: + return False + + +def cryptography_verify_certificate_signature(certificate, signer_public_key): + ''' + Check whether the given X509 certificate object was signed by the given public key object. + ''' + return cryptography_verify_signature( + certificate.signature, + certificate.tbs_certificate_bytes, + certificate.signature_hash_algorithm, + signer_public_key + ) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/math.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/math.py new file mode 100644 index 000000000..1cfe38b99 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/math.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import sys + + +def binary_exp_mod(f, e, m): + '''Computes f^e mod m in O(log e) multiplications modulo m.''' + # Compute len_e = floor(log_2(e)) + len_e = -1 + x = e + while x > 0: + x >>= 1 + len_e += 1 + # Compute f**e mod m + result = 1 + for k in range(len_e, -1, -1): + result = (result * result) % m + if ((e >> k) & 1) != 0: + result = (result * f) % m + return result + + +def simple_gcd(a, b): + '''Compute GCD of its two inputs.''' + while b != 0: + a, b = b, a % b + return a + + +def quick_is_not_prime(n): + '''Does some quick checks to see if we can poke a hole into the primality of n. + + A result of `False` does **not** mean that the number is prime; it just means + that we could not detect quickly whether it is not prime. + ''' + if n <= 2: + return True + # The constant in the next line is the product of all primes < 200 + if simple_gcd(n, 7799922041683461553249199106329813876687996789903550945093032474868511536164700810) > 1: + return True + # TODO: maybe do some iterations of Miller-Rabin to increase confidence + # (https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test) + return False + + +python_version = (sys.version_info[0], sys.version_info[1]) +if python_version >= (2, 7) or python_version >= (3, 1): + # Ansible still supports Python 2.6 on remote nodes + def count_bits(no): + no = abs(no) + if no == 0: + return 0 + return no.bit_length() +else: + # Slow, but works + def count_bits(no): + no = abs(no) + count = 0 + while no > 0: + no >>= 1 + count += 1 + return count diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate.py new file mode 100644 index 000000000..7a56d7e9d --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate.py @@ -0,0 +1,354 @@ +# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc +import traceback + +from ansible.module_utils import six +from ansible.module_utils.basic import missing_required_lib + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + load_privatekey, + load_certificate, + load_certificate_request, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_compare_public_keys, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import ( + get_certificate_info, +) + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' + +CRYPTOGRAPHY_IMP_ERR = None +CRYPTOGRAPHY_VERSION = None +try: + import cryptography + from cryptography import x509 + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + + +class CertificateError(OpenSSLObjectError): + pass + + +@six.add_metaclass(abc.ABCMeta) +class CertificateBackend(object): + def __init__(self, module, backend): + self.module = module + self.backend = backend + + self.force = module.params['force'] + self.ignore_timestamps = module.params['ignore_timestamps'] + 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.csr_path = module.params['csr_path'] + self.csr_content = module.params['csr_content'] + if self.csr_content is not None: + self.csr_content = self.csr_content.encode('utf-8') + + # The following are default values which make sure check() works as + # before if providers do not explicitly change these properties. + self.create_subject_key_identifier = 'never_create' + self.create_authority_key_identifier = False + + self.privatekey = None + self.csr = None + self.cert = None + self.existing_certificate = None + self.existing_certificate_bytes = None + + self.check_csr_subject = True + self.check_csr_extensions = True + + self.diff_before = self._get_info(None) + self.diff_after = self._get_info(None) + + def _get_info(self, data): + if data is None: + return dict() + try: + result = get_certificate_info(self.module, self.backend, data, prefer_one_fingerprint=True) + result['can_parse_certificate'] = True + return result + except Exception as exc: + return dict(can_parse_certificate=False) + + @abc.abstractmethod + def generate_certificate(self): + """(Re-)Generate certificate.""" + pass + + @abc.abstractmethod + def get_certificate_data(self): + """Return bytes for self.cert.""" + pass + + def set_existing(self, certificate_bytes): + """Set existing certificate bytes. None indicates that the key does not exist.""" + self.existing_certificate_bytes = certificate_bytes + self.diff_after = self.diff_before = self._get_info(self.existing_certificate_bytes) + + def has_existing(self): + """Query whether an existing certificate is/has been there.""" + return self.existing_certificate_bytes is not None + + def _ensure_private_key_loaded(self): + """Load the provided private key into self.privatekey.""" + if self.privatekey is not None: + return + if self.privatekey_path is None and self.privatekey_content is None: + return + try: + self.privatekey = load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend=self.backend, + ) + except OpenSSLBadPassphraseError as exc: + raise CertificateError(exc) + + def _ensure_csr_loaded(self): + """Load the CSR into self.csr.""" + if self.csr is not None: + return + if self.csr_path is None and self.csr_content is None: + return + self.csr = load_certificate_request( + path=self.csr_path, + content=self.csr_content, + backend=self.backend, + ) + + def _ensure_existing_certificate_loaded(self): + """Load the existing certificate into self.existing_certificate.""" + if self.existing_certificate is not None: + return + if self.existing_certificate_bytes is None: + return + self.existing_certificate = load_certificate( + path=None, + content=self.existing_certificate_bytes, + backend=self.backend, + ) + + def _check_privatekey(self): + """Check whether provided parameters match, assuming self.existing_certificate and self.privatekey have been populated.""" + if self.backend == 'cryptography': + return cryptography_compare_public_keys(self.existing_certificate.public_key(), self.privatekey.public_key()) + + def _check_csr(self): + """Check whether provided parameters match, assuming self.existing_certificate and self.csr have been populated.""" + if self.backend == 'cryptography': + # Verify that CSR is signed by certificate's private key + if not self.csr.is_signature_valid: + return False + if not cryptography_compare_public_keys(self.csr.public_key(), self.existing_certificate.public_key()): + return False + # Check subject + if self.check_csr_subject and self.csr.subject != self.existing_certificate.subject: + return False + # Check extensions + if not self.check_csr_extensions: + return True + cert_exts = list(self.existing_certificate.extensions) + csr_exts = list(self.csr.extensions) + if self.create_subject_key_identifier != 'never_create': + # Filter out SubjectKeyIdentifier extension before comparison + cert_exts = list(filter(lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier), cert_exts)) + csr_exts = list(filter(lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier), csr_exts)) + if self.create_authority_key_identifier: + # Filter out AuthorityKeyIdentifier extension before comparison + cert_exts = list(filter(lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier), cert_exts)) + csr_exts = list(filter(lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier), csr_exts)) + if len(cert_exts) != len(csr_exts): + return False + for cert_ext in cert_exts: + try: + csr_ext = self.csr.extensions.get_extension_for_oid(cert_ext.oid) + if cert_ext != csr_ext: + return False + except cryptography.x509.ExtensionNotFound as dummy: + return False + return True + + def _check_subject_key_identifier(self): + """Check whether Subject Key Identifier matches, assuming self.existing_certificate has been populated.""" + # Get hold of certificate's SKI + try: + ext = self.existing_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + except cryptography.x509.ExtensionNotFound as dummy: + return False + # Get hold of CSR's SKI for 'create_if_not_provided' + csr_ext = None + if self.create_subject_key_identifier == 'create_if_not_provided': + try: + csr_ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + except cryptography.x509.ExtensionNotFound as dummy: + pass + if csr_ext is None: + # If CSR had no SKI, or we chose to ignore it ('always_create'), compare with created SKI + if ext.value.digest != x509.SubjectKeyIdentifier.from_public_key(self.existing_certificate.public_key()).digest: + return False + else: + # If CSR had SKI and we did not ignore it ('create_if_not_provided'), compare SKIs + if ext.value.digest != csr_ext.value.digest: + return False + return True + + def needs_regeneration(self, not_before=None, not_after=None): + """Check whether a regeneration is necessary.""" + if self.force or self.existing_certificate_bytes is None: + return True + + try: + self._ensure_existing_certificate_loaded() + except Exception as dummy: + return True + + # Check whether private key matches + self._ensure_private_key_loaded() + if self.privatekey is not None and not self._check_privatekey(): + return True + + # Check whether CSR matches + self._ensure_csr_loaded() + if self.csr is not None and not self._check_csr(): + return True + + # Check SubjectKeyIdentifier + if self.create_subject_key_identifier != 'never_create' and not self._check_subject_key_identifier(): + return True + + # Check not before + if not_before is not None and not self.ignore_timestamps: + if self.existing_certificate.not_valid_before != not_before: + return True + + # Check not after + if not_after is not None and not self.ignore_timestamps: + if self.existing_certificate.not_valid_after != not_after: + return True + return False + + def dump(self, include_certificate): + """Serialize the object into a dictionary.""" + result = { + 'privatekey': self.privatekey_path, + 'csr': self.csr_path + } + # Get hold of certificate bytes + certificate_bytes = self.existing_certificate_bytes + if self.cert is not None: + certificate_bytes = self.get_certificate_data() + self.diff_after = self._get_info(certificate_bytes) + if include_certificate: + # Store result + result['certificate'] = certificate_bytes.decode('utf-8') if certificate_bytes else None + + result['diff'] = dict( + before=self.diff_before, + after=self.diff_after, + ) + return result + + +@six.add_metaclass(abc.ABCMeta) +class CertificateProvider(object): + @abc.abstractmethod + def validate_module_args(self, module): + """Check module arguments""" + + @abc.abstractmethod + def needs_version_two_certs(self, module): + """Whether the provider needs to create a version 2 certificate.""" + + @abc.abstractmethod + def create_backend(self, module, backend): + """Create an implementation for a backend. + + Return value must be instance of CertificateBackend. + """ + + +def select_backend(module, backend, provider): + """ + :type module: AnsibleModule + :type backend: str + :type provider: CertificateProvider + """ + provider.validate_module_args(module) + + 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) + + # If cryptography is available we'll use it + if can_use_cryptography: + backend = 'cryptography' + + # Fail if no backend has been found + if backend == 'auto': + module.fail_json(msg=("Cannot detect the required Python library " + "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) + + if backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + if provider.needs_version_two_certs(module): + module.fail_json(msg='The cryptography backend does not support v2 certificates') + + return provider.create_backend(module, backend) + + +def get_certificate_argument_spec(): + return ArgumentSpec( + argument_spec=dict( + provider=dict(type='str', choices=[]), # choices will be filled by add_XXX_provider_to_argument_spec() in certificate_xxx.py + force=dict(type='bool', default=False,), + csr_path=dict(type='path'), + csr_content=dict(type='str'), + ignore_timestamps=dict(type='bool', default=True), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']), + + # General properties of a certificate + privatekey_path=dict(type='path'), + privatekey_content=dict(type='str', no_log=True), + privatekey_passphrase=dict(type='str', no_log=True), + ), + mutually_exclusive=[ + ['csr_path', 'csr_content'], + ['privatekey_path', 'privatekey_content'], + ], + ) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_acme.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_acme.py new file mode 100644 index 000000000..18f30db54 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_acme.py @@ -0,0 +1,120 @@ +# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import os +import tempfile +import traceback + +from ansible.module_utils.common.text.converters import to_native, to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( + CertificateError, + CertificateBackend, + CertificateProvider, +) + + +class AcmeCertificateBackend(CertificateBackend): + def __init__(self, module, backend): + super(AcmeCertificateBackend, self).__init__(module, backend) + self.accountkey_path = module.params['acme_accountkey_path'] + self.challenge_path = module.params['acme_challenge_path'] + self.use_chain = module.params['acme_chain'] + self.acme_directory = module.params['acme_directory'] + + if self.csr_content is None and self.csr_path is None: + raise CertificateError( + 'csr_path or csr_content is required for ownca provider' + ) + if self.csr_content is None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file %s does not exist' % self.csr_path + ) + + if not os.path.exists(self.accountkey_path): + raise CertificateError( + 'The account key %s does not exist' % self.accountkey_path + ) + + if not os.path.exists(self.challenge_path): + raise CertificateError( + 'The challenge path %s does not exist' % self.challenge_path + ) + + self.acme_tiny_path = self.module.get_bin_path('acme-tiny', required=True) + + def generate_certificate(self): + """(Re-)Generate certificate.""" + + command = [self.acme_tiny_path] + if self.use_chain: + command.append('--chain') + command.extend(['--account-key', self.accountkey_path]) + if self.csr_content is not None: + # We need to temporarily write the CSR to disk + fd, tmpsrc = tempfile.mkstemp() + self.module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit + f = os.fdopen(fd, 'wb') + try: + f.write(self.csr_content) + except Exception as err: + try: + f.close() + except Exception as dummy: + pass + self.module.fail_json( + msg="failed to create temporary CSR file: %s" % to_native(err), + exception=traceback.format_exc() + ) + f.close() + command.extend(['--csr', tmpsrc]) + else: + command.extend(['--csr', self.csr_path]) + command.extend(['--acme-dir', self.challenge_path]) + command.extend(['--directory-url', self.acme_directory]) + + try: + self.cert = to_bytes(self.module.run_command(command, check_rc=True)[1]) + except OSError as exc: + raise CertificateError(exc) + + def get_certificate_data(self): + """Return bytes for self.cert.""" + return self.cert + + def dump(self, include_certificate): + result = super(AcmeCertificateBackend, self).dump(include_certificate) + result['accountkey'] = self.accountkey_path + return result + + +class AcmeCertificateProvider(CertificateProvider): + def validate_module_args(self, module): + if module.params['acme_accountkey_path'] is None: + module.fail_json(msg='The acme_accountkey_path option must be specified for the acme provider.') + if module.params['acme_challenge_path'] is None: + module.fail_json(msg='The acme_challenge_path option must be specified for the acme provider.') + + def needs_version_two_certs(self, module): + return False + + def create_backend(self, module, backend): + return AcmeCertificateBackend(module, backend) + + +def add_acme_provider_to_argument_spec(argument_spec): + argument_spec.argument_spec['provider']['choices'].append('acme') + argument_spec.argument_spec.update(dict( + acme_accountkey_path=dict(type='path'), + acme_challenge_path=dict(type='path'), + acme_chain=dict(type='bool', default=False), + acme_directory=dict(type='str', default="https://acme-v02.api.letsencrypt.org/directory"), + )) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_entrust.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_entrust.py new file mode 100644 index 000000000..baf53f5de --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_entrust.py @@ -0,0 +1,211 @@ +# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import datetime +import time +import os + +from ansible.module_utils.common.text.converters import to_native, to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.ecs.api import ECSClient, RestOperationException, SessionConfigurationException + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + load_certificate, + get_relative_time_option, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_serial_number_of_cert, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( + CertificateError, + CertificateBackend, + CertificateProvider, +) + +try: + from cryptography.x509.oid import NameOID +except ImportError: + pass + + +class EntrustCertificateBackend(CertificateBackend): + def __init__(self, module, backend): + super(EntrustCertificateBackend, self).__init__(module, backend) + self.trackingId = None + self.notAfter = get_relative_time_option(module.params['entrust_not_after'], 'entrust_not_after', backend=self.backend) + + if self.csr_content is None and self.csr_path is None: + raise CertificateError( + 'csr_path or csr_content is required for entrust provider' + ) + if self.csr_content is None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file {0} does not exist'.format(self.csr_path) + ) + + self._ensure_csr_loaded() + + # ECS API defaults to using the validated organization tied to the account. + # We want to always force behavior of trying to use the organization provided in the CSR. + # To that end we need to parse out the organization from the CSR. + self.csr_org = None + if self.backend == 'cryptography': + csr_subject_orgs = self.csr.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME) + if len(csr_subject_orgs) == 1: + self.csr_org = csr_subject_orgs[0].value + elif len(csr_subject_orgs) > 1: + self.module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations found in " + "Subject DN: '{0}'. ".format(self.csr.subject))) + # If no organization in the CSR, explicitly tell ECS that it should be blank in issued cert, not defaulted to + # organization tied to the account. + if self.csr_org is None: + self.csr_org = '' + + try: + self.ecs_client = ECSClient( + entrust_api_user=self.module.params['entrust_api_user'], + entrust_api_key=self.module.params['entrust_api_key'], + entrust_api_cert=self.module.params['entrust_api_client_cert_path'], + entrust_api_cert_key=self.module.params['entrust_api_client_cert_key_path'], + entrust_api_specification_path=self.module.params['entrust_api_specification_path'] + ) + except SessionConfigurationException as e: + module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e.message))) + + def generate_certificate(self): + """(Re-)Generate certificate.""" + body = {} + + # Read the CSR that was generated for us + if self.csr_content is not None: + # csr_content contains bytes + body['csr'] = to_native(self.csr_content) + else: + with open(self.csr_path, 'r') as csr_file: + body['csr'] = csr_file.read() + + body['certType'] = self.module.params['entrust_cert_type'] + + # Handle expiration (30 days if not specified) + expiry = self.notAfter + if not expiry: + gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())) + expiry = gmt_now + datetime.timedelta(days=365) + + expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z") + body['certExpiryDate'] = expiry_iso3339 + body['org'] = self.csr_org + body['tracking'] = { + 'requesterName': self.module.params['entrust_requester_name'], + 'requesterEmail': self.module.params['entrust_requester_email'], + 'requesterPhone': self.module.params['entrust_requester_phone'], + } + + try: + result = self.ecs_client.NewCertRequest(Body=body) + self.trackingId = result.get('trackingId') + except RestOperationException as e: + self.module.fail_json(msg='Failed to request new certificate from Entrust Certificate Services (ECS): {0}'.format(to_native(e.message))) + + self.cert_bytes = to_bytes(result.get('endEntityCert')) + self.cert = load_certificate(path=None, content=self.cert_bytes, backend=self.backend) + + def get_certificate_data(self): + """Return bytes for self.cert.""" + return self.cert_bytes + + def needs_regeneration(self): + parent_check = super(EntrustCertificateBackend, self).needs_regeneration() + + try: + cert_details = self._get_cert_details() + except RestOperationException as e: + self.module.fail_json(msg='Failed to get status of existing certificate from Entrust Certificate Services (ECS): {0}.'.format(to_native(e.message))) + + # Always issue a new certificate if the certificate is expired, suspended or revoked + status = cert_details.get('status', False) + if status == 'EXPIRED' or status == 'SUSPENDED' or status == 'REVOKED': + return True + + # If the requested cert type was specified and it is for a different certificate type than the initial certificate, a new one is needed + if self.module.params['entrust_cert_type'] and cert_details.get('certType') and self.module.params['entrust_cert_type'] != cert_details.get('certType'): + return True + + return parent_check + + def _get_cert_details(self): + cert_details = {} + try: + self._ensure_existing_certificate_loaded() + except Exception as dummy: + return + if self.existing_certificate: + serial_number = None + expiry = None + if self.backend == 'cryptography': + serial_number = "{0:X}".format(cryptography_serial_number_of_cert(self.existing_certificate)) + expiry = self.existing_certificate.not_valid_after + + # get some information about the expiry of this certificate + expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z") + cert_details['expiresAfter'] = expiry_iso3339 + + # If a trackingId is not already defined (from the result of a generate) + # use the serial number to identify the tracking Id + if self.trackingId is None and serial_number is not None: + cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {}) + + # Finding 0 or more than 1 result is a very unlikely use case, it simply means we cannot perform additional checks + # on the 'state' as returned by Entrust Certificate Services (ECS). The general certificate validity is + # still checked as it is in the rest of the module. + if len(cert_results) == 1: + self.trackingId = cert_results[0].get('trackingId') + + if self.trackingId is not None: + cert_details.update(self.ecs_client.GetCertificate(trackingId=self.trackingId)) + + return cert_details + + +class EntrustCertificateProvider(CertificateProvider): + def validate_module_args(self, module): + pass + + def needs_version_two_certs(self, module): + return False + + def create_backend(self, module, backend): + return EntrustCertificateBackend(module, backend) + + +def add_entrust_provider_to_argument_spec(argument_spec): + argument_spec.argument_spec['provider']['choices'].append('entrust') + argument_spec.argument_spec.update(dict( + entrust_cert_type=dict(type='str', default='STANDARD_SSL', + choices=['STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', + 'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT']), + entrust_requester_email=dict(type='str'), + entrust_requester_name=dict(type='str'), + entrust_requester_phone=dict(type='str'), + entrust_api_user=dict(type='str'), + entrust_api_key=dict(type='str', no_log=True), + entrust_api_client_cert_path=dict(type='path'), + entrust_api_client_cert_key_path=dict(type='path', no_log=True), + entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'), + entrust_not_after=dict(type='str', default='+365d'), + )) + argument_spec.required_if.append( + ['provider', 'entrust', ['entrust_requester_email', 'entrust_requester_name', 'entrust_requester_phone', + 'entrust_api_user', 'entrust_api_key', 'entrust_api_client_cert_path', + 'entrust_api_client_cert_key_path']] + ) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_info.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_info.py new file mode 100644 index 000000000..b10733ceb --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_info.py @@ -0,0 +1,411 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org> +# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at> +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc +import binascii +import datetime +import traceback + +from ansible.module_utils import six +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + load_certificate, + get_fingerprint_of_bytes, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_decode_name, + cryptography_get_extensions_from_cert, + cryptography_oid_to_name, + cryptography_serial_number_of_cert, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import ( + get_publickey_info, +) + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' + +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" + + +@six.add_metaclass(abc.ABCMeta) +class CertificateInfoRetrieval(object): + def __init__(self, module, backend, content): + # content must be a bytes string + self.module = module + self.backend = backend + self.content = content + + @abc.abstractmethod + def _get_der_bytes(self): + 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_pem(self): + pass + + @abc.abstractmethod + def _get_public_key_object(self): + 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 + + @abc.abstractmethod + def _get_issuer_uri(self): + pass + + def get_info(self, prefer_one_fingerprint=False, der_support_enabled=False): + result = dict() + self.cert = load_certificate(None, content=self.content, backend=self.backend, der_support_enabled=der_support_enabled) + + 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['public_key'] = to_native(self._get_public_key_pem()) + + public_key_info = get_publickey_info( + self.module, + self.backend, + key=self._get_public_key_object(), + prefer_one_fingerprint=prefer_one_fingerprint) + result.update({ + 'public_key_type': public_key_info['type'], + 'public_key_data': public_key_info['public_data'], + 'public_key_fingerprints': public_key_info['fingerprints'], + }) + + result['fingerprints'] = get_fingerprint_of_bytes( + self._get_der_bytes(), prefer_one=prefer_one_fingerprint) + + 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() + result['issuer_uri'] = self._get_issuer_uri() + + return result + + +class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval): + """Validate the supplied cert, using the cryptography backend""" + def __init__(self, module, content): + super(CertificateInfoRetrievalCryptography, self).__init__(module, 'cryptography', content) + self.name_encoding = module.params.get('name_encoding', 'ignore') + + def _get_der_bytes(self): + return self.cert.public_bytes(serialization.Encoding.DER) + + def _get_signature_algorithm(self): + return cryptography_oid_to_name(self.cert.signature_algorithm_oid) + + def _get_subject_ordered(self): + result = [] + for attribute in self.cert.subject: + result.append([cryptography_oid_to_name(attribute.oid), attribute.value]) + return result + + def _get_issuer_ordered(self): + result = [] + for attribute in self.cert.issuer: + result.append([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([ + 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: + # 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 = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) 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_pem(self): + return self.cert.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + def _get_public_key_object(self): + return self.cert.public_key() + + 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 = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) 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 cryptography_serial_number_of_cert(self.cert) + + def _get_all_extensions(self): + return 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 + + def _get_issuer_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.CA_ISSUERS: + if isinstance(desc.access_location, x509.UniformResourceIdentifier): + return desc.access_location.value + except x509.ExtensionNotFound as dummy: + pass + return None + + +def get_certificate_info(module, backend, content, prefer_one_fingerprint=False): + if backend == 'cryptography': + info = CertificateInfoRetrievalCryptography(module, content) + return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint) + + +def select_backend(module, backend, content): + if backend == 'auto': + # Detection what is possible + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + + # Try cryptography + if can_use_cryptography: + backend = 'cryptography' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Cannot detect any of the required Python libraries " + "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) + + if backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + return backend, CertificateInfoRetrievalCryptography(module, content) + else: + raise ValueError('Unsupported value for backend: {0}'.format(backend)) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_ownca.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_ownca.py new file mode 100644 index 000000000..ac1cf845a --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_ownca.py @@ -0,0 +1,276 @@ +# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import os + +from random import randrange + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + load_privatekey, + load_certificate, + get_relative_time_option, + select_message_digest, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_compare_public_keys, + cryptography_key_needs_digest_for_signing, + cryptography_serial_number_of_cert, + cryptography_verify_certificate_signature, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( + CRYPTOGRAPHY_VERSION, + CertificateError, + CertificateBackend, + CertificateProvider, +) + +try: + import cryptography + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.serialization import Encoding +except ImportError: + pass + + +class OwnCACertificateBackendCryptography(CertificateBackend): + def __init__(self, module): + super(OwnCACertificateBackendCryptography, self).__init__(module, 'cryptography') + + self.create_subject_key_identifier = module.params['ownca_create_subject_key_identifier'] + self.create_authority_key_identifier = module.params['ownca_create_authority_key_identifier'] + self.notBefore = get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) + self.notAfter = get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend) + self.digest = select_message_digest(module.params['ownca_digest']) + self.version = module.params['ownca_version'] + self.serial_number = x509.random_serial_number() + self.ca_cert_path = module.params['ownca_path'] + self.ca_cert_content = module.params['ownca_content'] + if self.ca_cert_content is not None: + self.ca_cert_content = self.ca_cert_content.encode('utf-8') + self.ca_privatekey_path = module.params['ownca_privatekey_path'] + self.ca_privatekey_content = module.params['ownca_privatekey_content'] + if self.ca_privatekey_content is not None: + self.ca_privatekey_content = self.ca_privatekey_content.encode('utf-8') + self.ca_privatekey_passphrase = module.params['ownca_privatekey_passphrase'] + + if self.csr_content is None and self.csr_path is None: + raise CertificateError( + 'csr_path or csr_content is required for ownca provider' + ) + if self.csr_content is None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file {0} does not exist'.format(self.csr_path) + ) + if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path): + raise CertificateError( + 'The CA certificate file {0} does not exist'.format(self.ca_cert_path) + ) + if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path): + raise CertificateError( + 'The CA private key file {0} does not exist'.format(self.ca_privatekey_path) + ) + + self._ensure_csr_loaded() + self.ca_cert = load_certificate( + path=self.ca_cert_path, + content=self.ca_cert_content, + backend=self.backend + ) + try: + self.ca_private_key = load_privatekey( + path=self.ca_privatekey_path, + content=self.ca_privatekey_content, + passphrase=self.ca_privatekey_passphrase, + backend=self.backend + ) + except OpenSSLBadPassphraseError as exc: + module.fail_json(msg=str(exc)) + + if not cryptography_compare_public_keys(self.ca_cert.public_key(), self.ca_private_key.public_key()): + raise CertificateError('The CA private key does not belong to the CA certificate') + + if cryptography_key_needs_digest_for_signing(self.ca_private_key): + if self.digest is None: + raise CertificateError( + 'The digest %s is not supported with the cryptography backend' % module.params['ownca_digest'] + ) + else: + self.digest = None + + def generate_certificate(self): + """(Re-)Generate certificate.""" + cert_builder = x509.CertificateBuilder() + cert_builder = cert_builder.subject_name(self.csr.subject) + cert_builder = cert_builder.issuer_name(self.ca_cert.subject) + cert_builder = cert_builder.serial_number(self.serial_number) + cert_builder = cert_builder.not_valid_before(self.notBefore) + cert_builder = cert_builder.not_valid_after(self.notAfter) + cert_builder = cert_builder.public_key(self.csr.public_key()) + has_ski = False + for extension in self.csr.extensions: + if isinstance(extension.value, x509.SubjectKeyIdentifier): + if self.create_subject_key_identifier == 'always_create': + continue + has_ski = True + if self.create_authority_key_identifier and isinstance(extension.value, x509.AuthorityKeyIdentifier): + continue + cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical) + if not has_ski and self.create_subject_key_identifier != 'never_create': + cert_builder = cert_builder.add_extension( + x509.SubjectKeyIdentifier.from_public_key(self.csr.public_key()), + critical=False + ) + if self.create_authority_key_identifier: + try: + ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + cert_builder = cert_builder.add_extension( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value) + if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext), + critical=False + ) + except cryptography.x509.ExtensionNotFound: + cert_builder = cert_builder.add_extension( + x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key()), + critical=False + ) + + try: + certificate = cert_builder.sign( + private_key=self.ca_private_key, algorithm=self.digest, + backend=default_backend() + ) + except TypeError as e: + if str(e) == 'Algorithm must be a registered hash algorithm.' and self.digest is None: + self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') + raise + + self.cert = certificate + + def get_certificate_data(self): + """Return bytes for self.cert.""" + return self.cert.public_bytes(Encoding.PEM) + + def needs_regeneration(self): + if super(OwnCACertificateBackendCryptography, self).needs_regeneration(not_before=self.notBefore, not_after=self.notAfter): + return True + + self._ensure_existing_certificate_loaded() + + # Check whether certificate is signed by CA certificate + if not cryptography_verify_certificate_signature(self.existing_certificate, self.ca_cert.public_key()): + return True + + # Check subject + if self.ca_cert.subject != self.existing_certificate.issuer: + return True + + # Check AuthorityKeyIdentifier + if self.create_authority_key_identifier: + try: + ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + expected_ext = ( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value) + if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext) + ) + except cryptography.x509.ExtensionNotFound: + expected_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key()) + + try: + ext = self.existing_certificate.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) + if ext.value != expected_ext: + return True + except cryptography.x509.ExtensionNotFound as dummy: + return True + + return False + + def dump(self, include_certificate): + result = super(OwnCACertificateBackendCryptography, self).dump(include_certificate) + result.update({ + 'ca_cert': self.ca_cert_path, + 'ca_privatekey': self.ca_privatekey_path, + }) + + if self.module.check_mode: + result.update({ + 'notBefore': self.notBefore.strftime("%Y%m%d%H%M%SZ"), + 'notAfter': self.notAfter.strftime("%Y%m%d%H%M%SZ"), + 'serial_number': self.serial_number, + }) + else: + if self.cert is None: + self.cert = self.existing_certificate + result.update({ + 'notBefore': self.cert.not_valid_before.strftime("%Y%m%d%H%M%SZ"), + 'notAfter': self.cert.not_valid_after.strftime("%Y%m%d%H%M%SZ"), + 'serial_number': cryptography_serial_number_of_cert(self.cert), + }) + + return result + + +def generate_serial_number(): + """Generate a serial number for a certificate""" + while True: + result = randrange(0, 1 << 160) + if result >= 1000: + return result + + +class OwnCACertificateProvider(CertificateProvider): + def validate_module_args(self, module): + if module.params['ownca_path'] is None and module.params['ownca_content'] is None: + module.fail_json(msg='One of ownca_path and ownca_content must be specified for the ownca provider.') + if module.params['ownca_privatekey_path'] is None and module.params['ownca_privatekey_content'] is None: + module.fail_json(msg='One of ownca_privatekey_path and ownca_privatekey_content must be specified for the ownca provider.') + + def needs_version_two_certs(self, module): + return module.params['ownca_version'] == 2 + + def create_backend(self, module, backend): + if backend == 'cryptography': + return OwnCACertificateBackendCryptography(module) + + +def add_ownca_provider_to_argument_spec(argument_spec): + argument_spec.argument_spec['provider']['choices'].append('ownca') + argument_spec.argument_spec.update(dict( + ownca_path=dict(type='path'), + ownca_content=dict(type='str'), + ownca_privatekey_path=dict(type='path'), + ownca_privatekey_content=dict(type='str', no_log=True), + ownca_privatekey_passphrase=dict(type='str', no_log=True), + ownca_digest=dict(type='str', default='sha256'), + ownca_version=dict(type='int', default=3), + ownca_not_before=dict(type='str', default='+0s'), + ownca_not_after=dict(type='str', default='+3650d'), + ownca_create_subject_key_identifier=dict( + type='str', + default='create_if_not_provided', + choices=['create_if_not_provided', 'always_create', 'never_create'] + ), + ownca_create_authority_key_identifier=dict(type='bool', default=True), + )) + argument_spec.mutually_exclusive.extend([ + ['ownca_path', 'ownca_content'], + ['ownca_privatekey_path', 'ownca_privatekey_content'], + ]) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py new file mode 100644 index 000000000..8695d43ee --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py @@ -0,0 +1,198 @@ +# -*- 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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import os + +from random import randrange + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + get_relative_time_option, + select_message_digest, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_key_needs_digest_for_signing, + cryptography_serial_number_of_cert, + cryptography_verify_certificate_signature, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( + CertificateError, + CertificateBackend, + CertificateProvider, +) + +try: + import cryptography + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.serialization import Encoding +except ImportError: + pass + + +class SelfSignedCertificateBackendCryptography(CertificateBackend): + def __init__(self, module): + super(SelfSignedCertificateBackendCryptography, self).__init__(module, 'cryptography') + + self.create_subject_key_identifier = module.params['selfsigned_create_subject_key_identifier'] + self.notBefore = get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) + self.notAfter = get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend) + self.digest = select_message_digest(module.params['selfsigned_digest']) + self.version = module.params['selfsigned_version'] + self.serial_number = x509.random_serial_number() + + if self.csr_path is not None and not os.path.exists(self.csr_path): + raise CertificateError( + 'The certificate signing request file {0} does not exist'.format(self.csr_path) + ) + if self.privatekey_content is None and not os.path.exists(self.privatekey_path): + raise CertificateError( + 'The private key file {0} does not exist'.format(self.privatekey_path) + ) + + self._module = module + + self._ensure_private_key_loaded() + + self._ensure_csr_loaded() + if self.csr is None: + # Create empty CSR on the fly + csr = cryptography.x509.CertificateSigningRequestBuilder() + csr = csr.subject_name(cryptography.x509.Name([])) + digest = None + if cryptography_key_needs_digest_for_signing(self.privatekey): + digest = self.digest + if digest is None: + self.module.fail_json(msg='Unsupported digest "{0}"'.format(module.params['selfsigned_digest'])) + try: + self.csr = csr.sign(self.privatekey, digest, default_backend()) + except TypeError as e: + if str(e) == 'Algorithm must be a registered hash algorithm.' and digest is None: + self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') + raise + + if cryptography_key_needs_digest_for_signing(self.privatekey): + if self.digest is None: + raise CertificateError( + 'The digest %s is not supported with the cryptography backend' % module.params['selfsigned_digest'] + ) + else: + self.digest = None + + def generate_certificate(self): + """(Re-)Generate certificate.""" + try: + cert_builder = x509.CertificateBuilder() + cert_builder = cert_builder.subject_name(self.csr.subject) + cert_builder = cert_builder.issuer_name(self.csr.subject) + cert_builder = cert_builder.serial_number(self.serial_number) + cert_builder = cert_builder.not_valid_before(self.notBefore) + cert_builder = cert_builder.not_valid_after(self.notAfter) + cert_builder = cert_builder.public_key(self.privatekey.public_key()) + has_ski = False + for extension in self.csr.extensions: + if isinstance(extension.value, x509.SubjectKeyIdentifier): + if self.create_subject_key_identifier == 'always_create': + continue + has_ski = True + cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical) + if not has_ski and self.create_subject_key_identifier != 'never_create': + cert_builder = cert_builder.add_extension( + x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()), + critical=False + ) + except ValueError as e: + raise CertificateError(str(e)) + + try: + certificate = cert_builder.sign( + private_key=self.privatekey, algorithm=self.digest, + backend=default_backend() + ) + except TypeError as e: + if str(e) == 'Algorithm must be a registered hash algorithm.' and self.digest is None: + self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') + raise + + self.cert = certificate + + def get_certificate_data(self): + """Return bytes for self.cert.""" + return self.cert.public_bytes(Encoding.PEM) + + def needs_regeneration(self): + if super(SelfSignedCertificateBackendCryptography, self).needs_regeneration(not_before=self.notBefore, not_after=self.notAfter): + return True + + self._ensure_existing_certificate_loaded() + + # Check whether certificate is signed by private key + if not cryptography_verify_certificate_signature(self.existing_certificate, self.privatekey.public_key()): + return True + + return False + + def dump(self, include_certificate): + result = super(SelfSignedCertificateBackendCryptography, self).dump(include_certificate) + + if self.module.check_mode: + result.update({ + 'notBefore': self.notBefore.strftime("%Y%m%d%H%M%SZ"), + 'notAfter': self.notAfter.strftime("%Y%m%d%H%M%SZ"), + 'serial_number': self.serial_number, + }) + else: + if self.cert is None: + self.cert = self.existing_certificate + result.update({ + 'notBefore': self.cert.not_valid_before.strftime("%Y%m%d%H%M%SZ"), + 'notAfter': self.cert.not_valid_after.strftime("%Y%m%d%H%M%SZ"), + 'serial_number': cryptography_serial_number_of_cert(self.cert), + }) + + return result + + +def generate_serial_number(): + """Generate a serial number for a certificate""" + while True: + result = randrange(0, 1 << 160) + if result >= 1000: + return result + + +class SelfSignedCertificateProvider(CertificateProvider): + def validate_module_args(self, module): + if module.params['privatekey_path'] is None and module.params['privatekey_content'] is None: + module.fail_json(msg='One of privatekey_path and privatekey_content must be specified for the selfsigned provider.') + + def needs_version_two_certs(self, module): + return module.params['selfsigned_version'] == 2 + + def create_backend(self, module, backend): + if backend == 'cryptography': + return SelfSignedCertificateBackendCryptography(module) + + +def add_selfsigned_provider_to_argument_spec(argument_spec): + argument_spec.argument_spec['provider']['choices'].append('selfsigned') + argument_spec.argument_spec.update(dict( + selfsigned_version=dict(type='int', default=3), + selfsigned_digest=dict(type='str', default='sha256'), + selfsigned_not_before=dict(type='str', default='+0s', aliases=['selfsigned_notBefore']), + selfsigned_not_after=dict(type='str', default='+3650d', aliases=['selfsigned_notAfter']), + selfsigned_create_subject_key_identifier=dict( + type='str', + default='create_if_not_provided', + choices=['create_if_not_provided', 'always_create', 'never_create'] + ), + )) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/common.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/common.py new file mode 100644 index 000000000..67f87dd0c --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/common.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible.module_utils.basic import AnsibleModule + + +class ArgumentSpec: + def __init__(self, argument_spec, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None): + self.argument_spec = argument_spec + self.mutually_exclusive = mutually_exclusive or [] + self.required_together = required_together or [] + self.required_one_of = required_one_of or [] + self.required_if = required_if or [] + self.required_by = required_by or {} + + def create_ansible_module_helper(self, clazz, args, **kwargs): + return clazz( + *args, + argument_spec=self.argument_spec, + mutually_exclusive=self.mutually_exclusive, + required_together=self.required_together, + required_one_of=self.required_one_of, + required_if=self.required_if, + required_by=self.required_by, + **kwargs) + + def create_ansible_module(self, **kwargs): + return self.create_ansible_module_helper(AnsibleModule, (), **kwargs) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/crl_info.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/crl_info.py new file mode 100644 index 000000000..a5b1b8ec5 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/crl_info.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import traceback + +from ansible.module_utils.basic import missing_required_lib + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_oid_to_name, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import ( + TIMESTAMP_FORMAT, + cryptography_decode_revoked_certificate, + cryptography_dump_revoked, + cryptography_get_signature_algorithm_oid_from_crl, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + identify_pem_format, +) + +# crypto_utils + +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 + + +class CRLInfoRetrieval(object): + def __init__(self, module, content, list_revoked_certificates=True): + # content must be a bytes string + self.module = module + self.content = content + self.list_revoked_certificates = list_revoked_certificates + self.name_encoding = module.params.get('name_encoding', 'ignore') + + def get_info(self): + self.crl_pem = identify_pem_format(self.content) + try: + if self.crl_pem: + self.crl = x509.load_pem_x509_crl(self.content, default_backend()) + else: + self.crl = x509.load_der_x509_crl(self.content, default_backend()) + except ValueError as e: + self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e)) + + result = { + 'changed': False, + 'format': 'pem' if self.crl_pem else 'der', + 'last_update': None, + 'next_update': None, + 'digest': None, + 'issuer_ordered': None, + 'issuer': None, + } + + result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT) + result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT) + result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl)) + issuer = [] + for attribute in self.crl.issuer: + issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value]) + result['issuer_ordered'] = issuer + result['issuer'] = {} + for k, v in issuer: + result['issuer'][k] = v + if self.list_revoked_certificates: + result['revoked_certificates'] = [] + for cert in self.crl: + entry = cryptography_decode_revoked_certificate(cert) + result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding)) + + return result + + +def get_crl_info(module, content, list_revoked_certificates=True): + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + + info = CRLInfoRetrieval(module, content, list_revoked_certificates=list_revoked_certificates) + return info.get_info() diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr.py new file mode 100644 index 000000000..4ab14e527 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr.py @@ -0,0 +1,675 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org> +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc +import binascii +import traceback + +from ansible.module_utils import six +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_native, to_text + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + load_privatekey, + load_certificate_request, + parse_name_field, + parse_ordered_name_field, + select_message_digest, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_get_basic_constraints, + cryptography_get_name, + cryptography_name_to_oid, + cryptography_key_needs_digest_for_signing, + cryptography_parse_key_usage_params, + cryptography_parse_relative_distinguished_name, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import ( + REVOCATION_REASON_MAP, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import ( + get_csr_info, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec + + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.3' + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + import cryptography.x509 + import cryptography.x509.oid + import cryptography.exceptions + import cryptography.hazmat.backends + import cryptography.hazmat.primitives.serialization + import cryptography.hazmat.primitives.hashes + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + CRYPTOGRAPHY_MUST_STAPLE_NAME = cryptography.x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24") + CRYPTOGRAPHY_MUST_STAPLE_VALUE = b"\x30\x03\x02\x01\x05" + + +class CertificateSigningRequestError(OpenSSLObjectError): + pass + + +# From the object called `module`, only the following properties are used: +# +# - module.params[] +# - module.warn(msg: str) +# - module.fail_json(msg: str, **kwargs) + + +@six.add_metaclass(abc.ABCMeta) +class CertificateSigningRequestBackend(object): + def __init__(self, module, backend): + self.module = module + self.backend = backend + self.digest = module.params['digest'] + 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.version = module.params['version'] + self.subjectAltName = module.params['subject_alt_name'] + self.subjectAltName_critical = module.params['subject_alt_name_critical'] + self.keyUsage = module.params['key_usage'] + self.keyUsage_critical = module.params['key_usage_critical'] + self.extendedKeyUsage = module.params['extended_key_usage'] + self.extendedKeyUsage_critical = module.params['extended_key_usage_critical'] + self.basicConstraints = module.params['basic_constraints'] + self.basicConstraints_critical = module.params['basic_constraints_critical'] + self.ocspMustStaple = module.params['ocsp_must_staple'] + self.ocspMustStaple_critical = module.params['ocsp_must_staple_critical'] + self.name_constraints_permitted = module.params['name_constraints_permitted'] or [] + self.name_constraints_excluded = module.params['name_constraints_excluded'] or [] + self.name_constraints_critical = module.params['name_constraints_critical'] + self.create_subject_key_identifier = module.params['create_subject_key_identifier'] + self.subject_key_identifier = module.params['subject_key_identifier'] + self.authority_key_identifier = module.params['authority_key_identifier'] + self.authority_cert_issuer = module.params['authority_cert_issuer'] + self.authority_cert_serial_number = module.params['authority_cert_serial_number'] + self.crl_distribution_points = module.params['crl_distribution_points'] + self.csr = None + self.privatekey = None + + if self.create_subject_key_identifier and self.subject_key_identifier is not None: + module.fail_json(msg='subject_key_identifier cannot be specified if create_subject_key_identifier is true') + + self.ordered_subject = False + self.subject = [ + ('C', module.params['country_name']), + ('ST', module.params['state_or_province_name']), + ('L', module.params['locality_name']), + ('O', module.params['organization_name']), + ('OU', module.params['organizational_unit_name']), + ('CN', module.params['common_name']), + ('emailAddress', module.params['email_address']), + ] + self.subject = [(entry[0], entry[1]) for entry in self.subject if entry[1]] + + try: + if module.params['subject']: + self.subject = self.subject + parse_name_field(module.params['subject'], 'subject') + if module.params['subject_ordered']: + if self.subject: + raise CertificateSigningRequestError('subject_ordered cannot be combined with any other subject field') + self.subject = parse_ordered_name_field(module.params['subject_ordered'], 'subject_ordered') + self.ordered_subject = True + except ValueError as exc: + raise CertificateSigningRequestError(to_native(exc)) + + self.using_common_name_for_san = False + if not self.subjectAltName and module.params['use_common_name_for_san']: + for sub in self.subject: + if sub[0] in ('commonName', 'CN'): + self.subjectAltName = ['DNS:%s' % sub[1]] + self.using_common_name_for_san = True + break + + if self.subject_key_identifier is not None: + try: + self.subject_key_identifier = binascii.unhexlify(self.subject_key_identifier.replace(':', '')) + except Exception as e: + raise CertificateSigningRequestError('Cannot parse subject_key_identifier: {0}'.format(e)) + + if self.authority_key_identifier is not None: + try: + self.authority_key_identifier = binascii.unhexlify(self.authority_key_identifier.replace(':', '')) + except Exception as e: + raise CertificateSigningRequestError('Cannot parse authority_key_identifier: {0}'.format(e)) + + self.existing_csr = None + self.existing_csr_bytes = None + + self.diff_before = self._get_info(None) + self.diff_after = self._get_info(None) + + def _get_info(self, data): + if data is None: + return dict() + try: + result = get_csr_info( + self.module, self.backend, data, validate_signature=False, prefer_one_fingerprint=True) + result['can_parse_csr'] = True + return result + except Exception as exc: + return dict(can_parse_csr=False) + + @abc.abstractmethod + def generate_csr(self): + """(Re-)Generate CSR.""" + pass + + @abc.abstractmethod + def get_csr_data(self): + """Return bytes for self.csr.""" + pass + + def set_existing(self, csr_bytes): + """Set existing CSR bytes. None indicates that the CSR does not exist.""" + self.existing_csr_bytes = csr_bytes + self.diff_after = self.diff_before = self._get_info(self.existing_csr_bytes) + + def has_existing(self): + """Query whether an existing CSR is/has been there.""" + return self.existing_csr_bytes is not None + + def _ensure_private_key_loaded(self): + """Load the provided private key into self.privatekey.""" + if self.privatekey is not None: + return + try: + self.privatekey = load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend=self.backend, + ) + except OpenSSLBadPassphraseError as exc: + raise CertificateSigningRequestError(exc) + + @abc.abstractmethod + def _check_csr(self): + """Check whether provided parameters, assuming self.existing_csr and self.privatekey have been populated.""" + pass + + def needs_regeneration(self): + """Check whether a regeneration is necessary.""" + if self.existing_csr_bytes is None: + return True + try: + self.existing_csr = load_certificate_request(None, content=self.existing_csr_bytes, backend=self.backend) + except Exception as dummy: + return True + self._ensure_private_key_loaded() + return not self._check_csr() + + def dump(self, include_csr): + """Serialize the object into a dictionary.""" + result = { + 'privatekey': self.privatekey_path, + 'subject': self.subject, + 'subjectAltName': self.subjectAltName, + 'keyUsage': self.keyUsage, + 'extendedKeyUsage': self.extendedKeyUsage, + 'basicConstraints': self.basicConstraints, + 'ocspMustStaple': self.ocspMustStaple, + 'name_constraints_permitted': self.name_constraints_permitted, + 'name_constraints_excluded': self.name_constraints_excluded, + } + # Get hold of CSR bytes + csr_bytes = self.existing_csr_bytes + if self.csr is not None: + csr_bytes = self.get_csr_data() + self.diff_after = self._get_info(csr_bytes) + if include_csr: + # Store result + result['csr'] = csr_bytes.decode('utf-8') if csr_bytes else None + + result['diff'] = dict( + before=self.diff_before, + after=self.diff_after, + ) + return result + + +def parse_crl_distribution_points(module, crl_distribution_points): + result = [] + for index, parse_crl_distribution_point in enumerate(crl_distribution_points): + try: + params = dict( + full_name=None, + relative_name=None, + crl_issuer=None, + reasons=None, + ) + if parse_crl_distribution_point['full_name'] is not None: + if not parse_crl_distribution_point['full_name']: + raise OpenSSLObjectError('full_name must not be empty') + params['full_name'] = [cryptography_get_name(name, 'full name') for name in parse_crl_distribution_point['full_name']] + if parse_crl_distribution_point['relative_name'] is not None: + if not parse_crl_distribution_point['relative_name']: + raise OpenSSLObjectError('relative_name must not be empty') + try: + params['relative_name'] = cryptography_parse_relative_distinguished_name(parse_crl_distribution_point['relative_name']) + except Exception: + # If cryptography's version is < 1.6, the error is probably caused by that + if CRYPTOGRAPHY_VERSION < LooseVersion('1.6'): + raise OpenSSLObjectError('Cannot specify relative_name for cryptography < 1.6') + raise + if parse_crl_distribution_point['crl_issuer'] is not None: + if not parse_crl_distribution_point['crl_issuer']: + raise OpenSSLObjectError('crl_issuer must not be empty') + params['crl_issuer'] = [cryptography_get_name(name, 'CRL issuer') for name in parse_crl_distribution_point['crl_issuer']] + if parse_crl_distribution_point['reasons'] is not None: + reasons = [] + for reason in parse_crl_distribution_point['reasons']: + reasons.append(REVOCATION_REASON_MAP[reason]) + params['reasons'] = frozenset(reasons) + result.append(cryptography.x509.DistributionPoint(**params)) + except (OpenSSLObjectError, ValueError) as e: + raise OpenSSLObjectError('Error while parsing CRL distribution point #{index}: {error}'.format(index=index, error=e)) + return result + + +# Implementation with using cryptography +class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBackend): + def __init__(self, module): + super(CertificateSigningRequestCryptographyBackend, self).__init__(module, 'cryptography') + self.cryptography_backend = cryptography.hazmat.backends.default_backend() + if self.version != 1: + module.warn('The cryptography backend only supports version 1. (The only valid value according to RFC 2986.)') + + if self.crl_distribution_points: + self.crl_distribution_points = parse_crl_distribution_points(module, self.crl_distribution_points) + + def generate_csr(self): + """(Re-)Generate CSR.""" + self._ensure_private_key_loaded() + + csr = cryptography.x509.CertificateSigningRequestBuilder() + try: + csr = csr.subject_name(cryptography.x509.Name([ + cryptography.x509.NameAttribute(cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.subject + ])) + except ValueError as e: + raise CertificateSigningRequestError(e) + + if self.subjectAltName: + csr = csr.add_extension(cryptography.x509.SubjectAlternativeName([ + cryptography_get_name(name) for name in self.subjectAltName + ]), critical=self.subjectAltName_critical) + + if self.keyUsage: + params = cryptography_parse_key_usage_params(self.keyUsage) + csr = csr.add_extension(cryptography.x509.KeyUsage(**params), critical=self.keyUsage_critical) + + if self.extendedKeyUsage: + usages = [cryptography_name_to_oid(usage) for usage in self.extendedKeyUsage] + csr = csr.add_extension(cryptography.x509.ExtendedKeyUsage(usages), critical=self.extendedKeyUsage_critical) + + if self.basicConstraints: + params = {} + ca, path_length = cryptography_get_basic_constraints(self.basicConstraints) + csr = csr.add_extension(cryptography.x509.BasicConstraints(ca, path_length), critical=self.basicConstraints_critical) + + if self.ocspMustStaple: + try: + # This only works with cryptography >= 2.1 + csr = csr.add_extension(cryptography.x509.TLSFeature([cryptography.x509.TLSFeatureType.status_request]), critical=self.ocspMustStaple_critical) + except AttributeError as dummy: + csr = csr.add_extension( + cryptography.x509.UnrecognizedExtension(CRYPTOGRAPHY_MUST_STAPLE_NAME, CRYPTOGRAPHY_MUST_STAPLE_VALUE), + critical=self.ocspMustStaple_critical + ) + + if self.name_constraints_permitted or self.name_constraints_excluded: + try: + csr = csr.add_extension(cryptography.x509.NameConstraints( + [cryptography_get_name(name, 'name constraints permitted') for name in self.name_constraints_permitted] or None, + [cryptography_get_name(name, 'name constraints excluded') for name in self.name_constraints_excluded] or None, + ), critical=self.name_constraints_critical) + except TypeError as e: + raise OpenSSLObjectError('Error while parsing name constraint: {0}'.format(e)) + + if self.create_subject_key_identifier: + csr = csr.add_extension( + cryptography.x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()), + critical=False + ) + elif self.subject_key_identifier is not None: + csr = csr.add_extension(cryptography.x509.SubjectKeyIdentifier(self.subject_key_identifier), critical=False) + + if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None: + issuers = None + if self.authority_cert_issuer is not None: + issuers = [cryptography_get_name(n, 'authority cert issuer') for n in self.authority_cert_issuer] + csr = csr.add_extension( + cryptography.x509.AuthorityKeyIdentifier(self.authority_key_identifier, issuers, self.authority_cert_serial_number), + critical=False + ) + + if self.crl_distribution_points: + csr = csr.add_extension( + cryptography.x509.CRLDistributionPoints(self.crl_distribution_points), + critical=False + ) + + digest = None + if cryptography_key_needs_digest_for_signing(self.privatekey): + digest = select_message_digest(self.digest) + if digest is None: + raise CertificateSigningRequestError('Unsupported digest "{0}"'.format(self.digest)) + try: + self.csr = csr.sign(self.privatekey, digest, self.cryptography_backend) + except TypeError as e: + if str(e) == 'Algorithm must be a registered hash algorithm.' and digest is None: + self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') + raise + except UnicodeError as e: + # This catches IDNAErrors, which happens when a bad name is passed as a SAN + # (https://github.com/ansible-collections/community.crypto/issues/105). + # For older cryptography versions, this is handled by idna, which raises + # an idna.core.IDNAError. Later versions of cryptography deprecated and stopped + # requiring idna, whence we cannot easily handle this error. Fortunately, in + # most versions of idna, IDNAError extends UnicodeError. There is only version + # 2.3 where it extends Exception instead (see + # https://github.com/kjd/idna/commit/ebefacd3134d0f5da4745878620a6a1cba86d130 + # and then + # https://github.com/kjd/idna/commit/ea03c7b5db7d2a99af082e0239da2b68aeea702a). + msg = 'Error while creating CSR: {0}\n'.format(e) + if self.using_common_name_for_san: + self.module.fail_json(msg=msg + 'This is probably caused because the Common Name is used as a SAN.' + ' Specifying use_common_name_for_san=false might fix this.') + self.module.fail_json(msg=msg + 'This is probably caused by an invalid Subject Alternative DNS Name.') + + def get_csr_data(self): + """Return bytes for self.csr.""" + return self.csr.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM) + + def _check_csr(self): + """Check whether provided parameters, assuming self.existing_csr and self.privatekey have been populated.""" + def _check_subject(csr): + subject = [(cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.subject] + current_subject = [(sub.oid, sub.value) for sub in csr.subject] + if self.ordered_subject: + return subject == current_subject + else: + return set(subject) == set(current_subject) + + def _find_extension(extensions, exttype): + return next( + (ext for ext in extensions if isinstance(ext.value, exttype)), + None + ) + + def _check_subjectAltName(extensions): + current_altnames_ext = _find_extension(extensions, cryptography.x509.SubjectAlternativeName) + current_altnames = [to_text(altname) for altname in current_altnames_ext.value] if current_altnames_ext else [] + altnames = [to_text(cryptography_get_name(altname)) for altname in self.subjectAltName] if self.subjectAltName else [] + if set(altnames) != set(current_altnames): + return False + if altnames: + if current_altnames_ext.critical != self.subjectAltName_critical: + return False + return True + + def _check_keyUsage(extensions): + current_keyusage_ext = _find_extension(extensions, cryptography.x509.KeyUsage) + if not self.keyUsage: + return current_keyusage_ext is None + elif current_keyusage_ext is None: + return False + params = cryptography_parse_key_usage_params(self.keyUsage) + for param in params: + if getattr(current_keyusage_ext.value, '_' + param) != params[param]: + return False + if current_keyusage_ext.critical != self.keyUsage_critical: + return False + return True + + def _check_extenededKeyUsage(extensions): + current_usages_ext = _find_extension(extensions, cryptography.x509.ExtendedKeyUsage) + current_usages = [str(usage) for usage in current_usages_ext.value] if current_usages_ext else [] + usages = [str(cryptography_name_to_oid(usage)) for usage in self.extendedKeyUsage] if self.extendedKeyUsage else [] + if set(current_usages) != set(usages): + return False + if usages: + if current_usages_ext.critical != self.extendedKeyUsage_critical: + return False + return True + + def _check_basicConstraints(extensions): + bc_ext = _find_extension(extensions, cryptography.x509.BasicConstraints) + current_ca = bc_ext.value.ca if bc_ext else False + current_path_length = bc_ext.value.path_length if bc_ext else None + ca, path_length = cryptography_get_basic_constraints(self.basicConstraints) + # Check CA flag + if ca != current_ca: + return False + # Check path length + if path_length != current_path_length: + return False + # Check criticality + if self.basicConstraints: + return bc_ext is not None and bc_ext.critical == self.basicConstraints_critical + else: + return bc_ext is None + + def _check_ocspMustStaple(extensions): + try: + # This only works with cryptography >= 2.1 + tlsfeature_ext = _find_extension(extensions, cryptography.x509.TLSFeature) + has_tlsfeature = True + except AttributeError as dummy: + tlsfeature_ext = next( + (ext for ext in extensions if ext.value.oid == CRYPTOGRAPHY_MUST_STAPLE_NAME), + None + ) + has_tlsfeature = False + if self.ocspMustStaple: + if not tlsfeature_ext or tlsfeature_ext.critical != self.ocspMustStaple_critical: + return False + if has_tlsfeature: + return cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value + else: + return tlsfeature_ext.value.value == CRYPTOGRAPHY_MUST_STAPLE_VALUE + else: + return tlsfeature_ext is None + + def _check_nameConstraints(extensions): + current_nc_ext = _find_extension(extensions, cryptography.x509.NameConstraints) + current_nc_perm = [to_text(altname) for altname in current_nc_ext.value.permitted_subtrees or []] if current_nc_ext else [] + current_nc_excl = [to_text(altname) for altname in current_nc_ext.value.excluded_subtrees or []] if current_nc_ext else [] + nc_perm = [to_text(cryptography_get_name(altname, 'name constraints permitted')) for altname in self.name_constraints_permitted] + nc_excl = [to_text(cryptography_get_name(altname, 'name constraints excluded')) for altname in self.name_constraints_excluded] + if set(nc_perm) != set(current_nc_perm) or set(nc_excl) != set(current_nc_excl): + return False + if nc_perm or nc_excl: + if current_nc_ext.critical != self.name_constraints_critical: + return False + return True + + def _check_subject_key_identifier(extensions): + ext = _find_extension(extensions, cryptography.x509.SubjectKeyIdentifier) + if self.create_subject_key_identifier or self.subject_key_identifier is not None: + if not ext or ext.critical: + return False + if self.create_subject_key_identifier: + digest = cryptography.x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()).digest + return ext.value.digest == digest + else: + return ext.value.digest == self.subject_key_identifier + else: + return ext is None + + def _check_authority_key_identifier(extensions): + ext = _find_extension(extensions, cryptography.x509.AuthorityKeyIdentifier) + if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None: + if not ext or ext.critical: + return False + aci = None + csr_aci = None + if self.authority_cert_issuer is not None: + aci = [to_text(cryptography_get_name(n, 'authority cert issuer')) for n in self.authority_cert_issuer] + if ext.value.authority_cert_issuer is not None: + csr_aci = [to_text(n) for n in ext.value.authority_cert_issuer] + return (ext.value.key_identifier == self.authority_key_identifier + and csr_aci == aci + and ext.value.authority_cert_serial_number == self.authority_cert_serial_number) + else: + return ext is None + + def _check_crl_distribution_points(extensions): + ext = _find_extension(extensions, cryptography.x509.CRLDistributionPoints) + if self.crl_distribution_points is None: + return ext is None + if not ext: + return False + return list(ext.value) == self.crl_distribution_points + + def _check_extensions(csr): + extensions = csr.extensions + return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and + _check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and + _check_ocspMustStaple(extensions) and _check_subject_key_identifier(extensions) and + _check_authority_key_identifier(extensions) and _check_nameConstraints(extensions) and + _check_crl_distribution_points(extensions)) + + def _check_signature(csr): + if not csr.is_signature_valid: + return False + # To check whether public key of CSR belongs to private key, + # encode both public keys and compare PEMs. + key_a = csr.public_key().public_bytes( + cryptography.hazmat.primitives.serialization.Encoding.PEM, + cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo + ) + key_b = self.privatekey.public_key().public_bytes( + cryptography.hazmat.primitives.serialization.Encoding.PEM, + cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo + ) + return key_a == key_b + + return _check_subject(self.existing_csr) and _check_extensions(self.existing_csr) and _check_signature(self.existing_csr) + + +def select_backend(module, backend): + if backend == 'auto': + # Detection what is possible + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + + # Try cryptography + if can_use_cryptography: + backend = 'cryptography' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Cannot detect any of the required Python libraries " + "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) + + if backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + return backend, CertificateSigningRequestCryptographyBackend(module) + else: + raise Exception('Unsupported value for backend: {0}'.format(backend)) + + +def get_csr_argument_spec(): + return ArgumentSpec( + argument_spec=dict( + digest=dict(type='str', default='sha256'), + privatekey_path=dict(type='path'), + privatekey_content=dict(type='str', no_log=True), + privatekey_passphrase=dict(type='str', no_log=True), + version=dict(type='int', default=1, choices=[1]), + subject=dict(type='dict'), + subject_ordered=dict(type='list', elements='dict'), + country_name=dict(type='str', aliases=['C', 'countryName']), + state_or_province_name=dict(type='str', aliases=['ST', 'stateOrProvinceName']), + locality_name=dict(type='str', aliases=['L', 'localityName']), + organization_name=dict(type='str', aliases=['O', 'organizationName']), + organizational_unit_name=dict(type='str', aliases=['OU', 'organizationalUnitName']), + common_name=dict(type='str', aliases=['CN', 'commonName']), + email_address=dict(type='str', aliases=['E', 'emailAddress']), + subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName']), + subject_alt_name_critical=dict(type='bool', default=False, aliases=['subjectAltName_critical']), + use_common_name_for_san=dict(type='bool', default=True, aliases=['useCommonNameForSAN']), + key_usage=dict(type='list', elements='str', aliases=['keyUsage']), + key_usage_critical=dict(type='bool', default=False, aliases=['keyUsage_critical']), + extended_key_usage=dict(type='list', elements='str', aliases=['extKeyUsage', 'extendedKeyUsage']), + extended_key_usage_critical=dict(type='bool', default=False, aliases=['extKeyUsage_critical', 'extendedKeyUsage_critical']), + basic_constraints=dict(type='list', elements='str', aliases=['basicConstraints']), + basic_constraints_critical=dict(type='bool', default=False, aliases=['basicConstraints_critical']), + ocsp_must_staple=dict(type='bool', default=False, aliases=['ocspMustStaple']), + ocsp_must_staple_critical=dict(type='bool', default=False, aliases=['ocspMustStaple_critical']), + name_constraints_permitted=dict(type='list', elements='str'), + name_constraints_excluded=dict(type='list', elements='str'), + name_constraints_critical=dict(type='bool', default=False), + create_subject_key_identifier=dict(type='bool', default=False), + subject_key_identifier=dict(type='str'), + authority_key_identifier=dict(type='str'), + authority_cert_issuer=dict(type='list', elements='str'), + authority_cert_serial_number=dict(type='int'), + crl_distribution_points=dict( + type='list', + elements='dict', + options=dict( + full_name=dict(type='list', elements='str'), + relative_name=dict(type='list', elements='str'), + crl_issuer=dict(type='list', elements='str'), + reasons=dict(type='list', elements='str', choices=[ + 'key_compromise', + 'ca_compromise', + 'affiliation_changed', + 'superseded', + 'cessation_of_operation', + 'certificate_hold', + 'privilege_withdrawn', + 'aa_compromise', + ]), + ), + mutually_exclusive=[('full_name', 'relative_name')], + required_one_of=[('full_name', 'relative_name', 'crl_issuer')], + ), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']), + ), + required_together=[ + ['authority_cert_issuer', 'authority_cert_serial_number'], + ], + mutually_exclusive=[ + ['privatekey_path', 'privatekey_content'], + ['subject', 'subject_ordered'], + ], + required_one_of=[ + ['privatekey_path', 'privatekey_content'], + ], + ) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr_info.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr_info.py new file mode 100644 index 000000000..fc3d0d3dc --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr_info.py @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org> +# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at> +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc +import binascii +import traceback + +from ansible.module_utils import six +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + load_certificate_request, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_decode_name, + cryptography_get_extensions_from_csr, + cryptography_oid_to_name, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import ( + get_publickey_info, +) + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.3' + +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" + + +@six.add_metaclass(abc.ABCMeta) +class CSRInfoRetrieval(object): + def __init__(self, module, backend, content, validate_signature): + # content must be a bytes string + self.module = module + self.backend = backend + self.content = content + self.validate_signature = validate_signature + + @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_name_constraints(self): + pass + + @abc.abstractmethod + def _get_public_key_pem(self): + pass + + @abc.abstractmethod + def _get_public_key_object(self): + 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, prefer_one_fingerprint=False): + result = dict() + self.csr = load_certificate_request(None, 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['name_constraints_permitted'], + result['name_constraints_excluded'], + result['name_constraints_critical'], + ) = self._get_name_constraints() + + result['public_key'] = to_native(self._get_public_key_pem()) + + public_key_info = get_publickey_info( + self.module, + self.backend, + key=self._get_public_key_object(), + prefer_one_fingerprint=prefer_one_fingerprint) + result.update({ + 'public_key_type': public_key_info['type'], + 'public_key_data': public_key_info['public_data'], + 'public_key_fingerprints': public_key_info['fingerprints'], + }) + + 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 self.validate_signature and not result['signature_valid']: + self.module.fail_json( + msg='CSR signature is invalid!', + **result + ) + return result + + +class CSRInfoRetrievalCryptography(CSRInfoRetrieval): + """Validate the supplied CSR, using the cryptography backend""" + def __init__(self, module, content, validate_signature): + super(CSRInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, validate_signature) + self.name_encoding = module.params.get('name_encoding', 'ignore') + + def _get_subject_ordered(self): + result = [] + for attribute in self.csr.subject: + result.append([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([ + 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 = ['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: + # 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 = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in san_ext.value] + return result, san_ext.critical + except cryptography.x509.ExtensionNotFound: + return None, False + + def _get_name_constraints(self): + try: + nc_ext = self.csr.extensions.get_extension_for_class(x509.NameConstraints) + permitted = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in nc_ext.value.permitted_subtrees or []] + excluded = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in nc_ext.value.excluded_subtrees or []] + return permitted, excluded, nc_ext.critical + except cryptography.x509.ExtensionNotFound: + return None, None, False + + def _get_public_key_pem(self): + return self.csr.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + def _get_public_key_object(self): + return self.csr.public_key() + + 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 = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) 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 cryptography_get_extensions_from_csr(self.csr) + + def _is_signature_valid(self): + return self.csr.is_signature_valid + + +def get_csr_info(module, backend, content, validate_signature=True, prefer_one_fingerprint=False): + if backend == 'cryptography': + info = CSRInfoRetrievalCryptography(module, content, validate_signature=validate_signature) + return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint) + + +def select_backend(module, backend, content, validate_signature=True): + if backend == 'auto': + # Detection what is possible + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + + # Try cryptography + if can_use_cryptography: + backend = 'cryptography' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Cannot detect the required Python library " + "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) + + if backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + return backend, CSRInfoRetrievalCryptography(module, content, validate_signature=validate_signature) + else: + raise ValueError('Unsupported value for backend: {0}'.format(backend)) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey.py new file mode 100644 index 000000000..dc13107b7 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey.py @@ -0,0 +1,533 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org> +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc +import base64 +import traceback + +from ansible.module_utils import six +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + CRYPTOGRAPHY_HAS_X25519, + CRYPTOGRAPHY_HAS_X25519_FULL, + CRYPTOGRAPHY_HAS_X448, + CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED448, + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + get_fingerprint_of_privatekey, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + identify_private_key_format, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import ( + PrivateKeyConsistencyError, + PrivateKeyParseError, + get_privatekey_info, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec + + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' + +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 + + +class PrivateKeyError(OpenSSLObjectError): + pass + + +# From the object called `module`, only the following properties are used: +# +# - module.params[] +# - module.warn(msg: str) +# - module.fail_json(msg: str, **kwargs) + + +@six.add_metaclass(abc.ABCMeta) +class PrivateKeyBackend: + def __init__(self, module, backend): + self.module = module + self.type = module.params['type'] + self.size = module.params['size'] + self.curve = module.params['curve'] + self.passphrase = module.params['passphrase'] + self.cipher = module.params['cipher'] + self.format = module.params['format'] + self.format_mismatch = module.params.get('format_mismatch', 'regenerate') + self.regenerate = module.params.get('regenerate', 'full_idempotence') + self.backend = backend + + self.private_key = None + + self.existing_private_key = None + self.existing_private_key_bytes = None + + self.diff_before = self._get_info(None) + self.diff_after = self._get_info(None) + + def _get_info(self, data): + if data is None: + return dict() + result = dict(can_parse_key=False) + try: + result.update(get_privatekey_info( + self.module, self.backend, data, passphrase=self.passphrase, + return_private_key_data=False, prefer_one_fingerprint=True)) + except PrivateKeyConsistencyError as exc: + result.update(exc.result) + except PrivateKeyParseError as exc: + result.update(exc.result) + except Exception as exc: + pass + return result + + @abc.abstractmethod + def generate_private_key(self): + """(Re-)Generate private key.""" + pass + + def convert_private_key(self): + """Convert existing private key (self.existing_private_key) to new private key (self.private_key). + + This is effectively a copy without active conversion. The conversion is done + during load and store; get_private_key_data() uses the destination format to + serialize the key. + """ + self._ensure_existing_private_key_loaded() + self.private_key = self.existing_private_key + + @abc.abstractmethod + def get_private_key_data(self): + """Return bytes for self.private_key.""" + pass + + def set_existing(self, privatekey_bytes): + """Set existing private key bytes. None indicates that the key does not exist.""" + self.existing_private_key_bytes = privatekey_bytes + self.diff_after = self.diff_before = self._get_info(self.existing_private_key_bytes) + + def has_existing(self): + """Query whether an existing private key is/has been there.""" + return self.existing_private_key_bytes is not None + + @abc.abstractmethod + def _check_passphrase(self): + """Check whether provided passphrase matches, assuming self.existing_private_key_bytes has been populated.""" + pass + + @abc.abstractmethod + def _ensure_existing_private_key_loaded(self): + """Make sure that self.existing_private_key is populated from self.existing_private_key_bytes.""" + pass + + @abc.abstractmethod + def _check_size_and_type(self): + """Check whether provided size and type matches, assuming self.existing_private_key has been populated.""" + pass + + @abc.abstractmethod + def _check_format(self): + """Check whether the key file format, assuming self.existing_private_key and self.existing_private_key_bytes has been populated.""" + pass + + def needs_regeneration(self): + """Check whether a regeneration is necessary.""" + if self.regenerate == 'always': + return True + if not self.has_existing(): + # key does not exist + return True + if not self._check_passphrase(): + if self.regenerate == 'full_idempotence': + return True + self.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=true`.') + self._ensure_existing_private_key_loaded() + if self.regenerate != 'never': + if not self._check_size_and_type(): + if self.regenerate in ('partial_idempotence', 'full_idempotence'): + return True + self.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=true`.') + # During generation step, regenerate if format does not match and format_mismatch == 'regenerate' + if self.format_mismatch == 'regenerate' and self.regenerate != 'never': + if not self._check_format(): + if self.regenerate in ('partial_idempotence', 'full_idempotence'): + return True + self.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=true`.' + ' To convert the key, set `format_mismatch` to `convert`.') + return False + + def needs_conversion(self): + """Check whether a conversion is necessary. Must only be called if needs_regeneration() returned False.""" + # During conversion step, convert if format does not match and format_mismatch == 'convert' + self._ensure_existing_private_key_loaded() + return self.has_existing() and self.format_mismatch == 'convert' and not self._check_format() + + def _get_fingerprint(self): + if self.private_key: + return get_fingerprint_of_privatekey(self.private_key, backend=self.backend) + try: + self._ensure_existing_private_key_loaded() + except Exception as dummy: + # Ignore errors + pass + if self.existing_private_key: + return get_fingerprint_of_privatekey(self.existing_private_key, backend=self.backend) + + def dump(self, include_key): + """Serialize the object into a dictionary.""" + + if not self.private_key: + try: + self._ensure_existing_private_key_loaded() + except Exception as dummy: + # Ignore errors + pass + result = { + 'type': self.type, + 'size': self.size, + 'fingerprint': self._get_fingerprint(), + } + if self.type == 'ECC': + result['curve'] = self.curve + # Get hold of private key bytes + pk_bytes = self.existing_private_key_bytes + if self.private_key is not None: + pk_bytes = self.get_private_key_data() + self.diff_after = self._get_info(pk_bytes) + if include_key: + # Store result + if pk_bytes: + if identify_private_key_format(pk_bytes) == 'raw': + result['privatekey'] = base64.b64encode(pk_bytes) + else: + result['privatekey'] = pk_bytes.decode('utf-8') + else: + result['privatekey'] = None + + result['diff'] = dict( + before=self.diff_before, + after=self.diff_after, + ) + return result + + +# Implementation with using cryptography +class PrivateKeyCryptographyBackend(PrivateKeyBackend): + + 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(PrivateKeyCryptographyBackend, self).__init__(module=module, backend='cryptography') + + self.curves = dict() + self._add_curve('secp224r1', 'SECP224R1') + self._add_curve('secp256k1', 'SECP256K1') + self._add_curve('secp256r1', 'SECP256R1') + self._add_curve('secp384r1', 'SECP384R1') + self._add_curve('secp521r1', 'SECP521R1') + self._add_curve('secp192r1', 'SECP192R1', deprecated=True) + self._add_curve('sect163k1', 'SECT163K1', deprecated=True) + self._add_curve('sect163r2', 'SECT163R2', deprecated=True) + self._add_curve('sect233k1', 'SECT233K1', deprecated=True) + self._add_curve('sect233r1', 'SECT233R1', deprecated=True) + self._add_curve('sect283k1', 'SECT283K1', deprecated=True) + self._add_curve('sect283r1', 'SECT283R1', deprecated=True) + self._add_curve('sect409k1', 'SECT409K1', deprecated=True) + self._add_curve('sect409r1', 'SECT409R1', deprecated=True) + self._add_curve('sect571k1', 'SECT571K1', deprecated=True) + self._add_curve('sect571r1', 'SECT571R1', deprecated=True) + self._add_curve('brainpoolP256r1', 'BrainpoolP256R1', deprecated=True) + self._add_curve('brainpoolP384r1', 'BrainpoolP384R1', deprecated=True) + self._add_curve('brainpoolP512r1', 'BrainpoolP512R1', deprecated=True) + + self.cryptography_backend = cryptography.hazmat.backends.default_backend() + + 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.private_key = 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.private_key = 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.private_key = cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate() + if CRYPTOGRAPHY_HAS_X448 and self.type == 'X448': + self.private_key = cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate() + if CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519': + self.private_key = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate() + if CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448': + self.private_key = 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.private_key = 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 get_private_key_data(self): + """Return bytes for self.private_key""" + # 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.private_key.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): + data = self.existing_private_key_bytes + try: + # Interpret bytes depending on format. + format = 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 _ensure_existing_private_key_loaded(self): + if self.existing_private_key is None and self.has_existing(): + self.existing_private_key = self._load_privatekey() + + def _check_passphrase(self): + try: + format = identify_private_key_format(self.existing_private_key_bytes) + 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( + self.existing_private_key_bytes, + 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): + if isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + return self.type == 'RSA' and self.size == self.existing_private_key.key_size + if isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): + return self.type == 'DSA' and self.size == self.existing_private_key.key_size + if CRYPTOGRAPHY_HAS_X25519 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey): + return self.type == 'X25519' + if CRYPTOGRAPHY_HAS_X448 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey): + return self.type == 'X448' + if CRYPTOGRAPHY_HAS_ED25519 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): + return self.type == 'Ed25519' + if CRYPTOGRAPHY_HAS_ED448 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): + return self.type == 'Ed448' + if isinstance(self.existing_private_key, 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.existing_private_key) + + return False + + def _check_format(self): + if self.format == 'auto_ignore': + return True + try: + format = identify_private_key_format(self.existing_private_key_bytes) + return format == self._get_wanted_format() + except Exception as dummy: + return False + + +def select_backend(module, backend): + if backend == 'auto': + # Detection what is possible + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + + # Decision + if can_use_cryptography: + backend = 'cryptography' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Cannot detect the required Python library " + "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) + if backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + return backend, PrivateKeyCryptographyBackend(module) + else: + raise Exception('Unsupported value for backend: {0}'.format(backend)) + + +def get_privatekey_argument_spec(): + return ArgumentSpec( + argument_spec=dict( + 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=[ + 'secp224r1', 'secp256k1', 'secp256r1', 'secp384r1', 'secp521r1', + 'secp192r1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', + 'sect163k1', 'sect163r2', 'sect233k1', 'sect233r1', 'sect283k1', + 'sect283r1', 'sect409k1', 'sect409r1', 'sect571k1', 'sect571r1', + ]), + passphrase=dict(type='str', no_log=True), + cipher=dict(type='str'), + 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', 'cryptography'], default='auto'), + regenerate=dict( + type='str', + default='full_idempotence', + choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always'] + ), + ), + required_together=[ + ['cipher', 'passphrase'] + ], + required_if=[ + ['type', 'ECC', ['curve']], + ], + ) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_convert.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_convert.py new file mode 100644 index 000000000..905ca70fe --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_convert.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc +import traceback + +from ansible.module_utils import six +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + CRYPTOGRAPHY_HAS_X25519, + CRYPTOGRAPHY_HAS_X448, + CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED448, + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_compare_private_keys, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + identify_private_key_format, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec + + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' + +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 + + +class PrivateKeyError(OpenSSLObjectError): + pass + + +# From the object called `module`, only the following properties are used: +# +# - module.params[] +# - module.warn(msg: str) +# - module.fail_json(msg: str, **kwargs) + + +@six.add_metaclass(abc.ABCMeta) +class PrivateKeyConvertBackend: + def __init__(self, module, backend): + self.module = module + self.src_path = module.params['src_path'] + self.src_content = module.params['src_content'] + self.src_passphrase = module.params['src_passphrase'] + self.format = module.params['format'] + self.dest_passphrase = module.params['dest_passphrase'] + self.backend = backend + + self.src_private_key = None + if self.src_path is not None: + self.src_private_key_bytes = load_file(self.src_path, module) + else: + self.src_private_key_bytes = self.src_content.encode('utf-8') + + self.dest_private_key = None + self.dest_private_key_bytes = None + + @abc.abstractmethod + def get_private_key_data(self): + """Return bytes for self.src_private_key in output format.""" + pass + + def set_existing_destination(self, privatekey_bytes): + """Set existing private key bytes. None indicates that the key does not exist.""" + self.dest_private_key_bytes = privatekey_bytes + + def has_existing_destination(self): + """Query whether an existing private key is/has been there.""" + return self.dest_private_key_bytes is not None + + @abc.abstractmethod + def _load_private_key(self, data, passphrase, current_hint=None): + """Check whether data cna be loaded as a private key with the provided passphrase. Return tuple (type, private_key).""" + pass + + def needs_conversion(self): + """Check whether a conversion is necessary. Must only be called if needs_regeneration() returned False.""" + dummy, self.src_private_key = self._load_private_key(self.src_private_key_bytes, self.src_passphrase) + + if not self.has_existing_destination(): + return True + + try: + format, self.dest_private_key = self._load_private_key(self.dest_private_key_bytes, self.dest_passphrase, current_hint=self.src_private_key) + except Exception: + return True + + return format != self.format or not cryptography_compare_private_keys(self.dest_private_key, self.src_private_key) + + def dump(self): + """Serialize the object into a dictionary.""" + return {} + + +# Implementation with using cryptography +class PrivateKeyConvertCryptographyBackend(PrivateKeyConvertBackend): + def __init__(self, module): + super(PrivateKeyConvertCryptographyBackend, self).__init__(module=module, backend='cryptography') + + self.cryptography_backend = cryptography.hazmat.backends.default_backend() + + def get_private_key_data(self): + """Return bytes for self.src_private_key in output format""" + # Select export format and encoding + try: + export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM + if self.format == 'pkcs1': + # "TraditionalOpenSSL" format is PKCS1 + export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL + elif self.format == 'pkcs8': + export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8 + elif self.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.dest_passphrase: + encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(to_bytes(self.dest_passphrase)) + + # Serialize key + try: + return self.src_private_key.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_private_key(self, data, passphrase, current_hint=None): + try: + # Interpret bytes depending on format. + format = identify_private_key_format(data) + if format == 'raw': + if passphrase is not None: + raise PrivateKeyError('Cannot load raw key with passphrase') + if len(data) == 56 and CRYPTOGRAPHY_HAS_X448: + return format, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data) + if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448: + return format, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data) + if len(data) == 32: + if CRYPTOGRAPHY_HAS_X25519 and not CRYPTOGRAPHY_HAS_ED25519: + return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) + if CRYPTOGRAPHY_HAS_ED25519 and not CRYPTOGRAPHY_HAS_X25519: + return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) + if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519: + if isinstance(current_hint, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey): + try: + return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) + except Exception: + return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) + else: + try: + return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) + except Exception: + return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) + raise PrivateKeyError('Cannot load raw key') + else: + return format, cryptography.hazmat.primitives.serialization.load_pem_private_key( + data, + None if passphrase is None else to_bytes(passphrase), + backend=self.cryptography_backend + ) + except Exception as e: + raise PrivateKeyError(e) + + +def select_backend(module): + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + return PrivateKeyConvertCryptographyBackend(module) + + +def get_privatekey_argument_spec(): + return ArgumentSpec( + argument_spec=dict( + src_path=dict(type='path'), + src_content=dict(type='str'), + src_passphrase=dict(type='str', no_log=True), + dest_passphrase=dict(type='str', no_log=True), + format=dict(type='str', required=True, choices=['pkcs1', 'pkcs8', 'raw']), + ), + mutually_exclusive=[ + ['src_path', 'src_content'], + ], + required_one_of=[ + ['src_path', 'src_content'], + ], + ) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_info.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_info.py new file mode 100644 index 000000000..d87b9c2be --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_info.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org> +# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at> +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc +import traceback + +from ansible.module_utils import six +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_native, to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED448, + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + load_privatekey, + get_fingerprint_of_bytes, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.math import ( + binary_exp_mod, + quick_is_not_prime, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import ( + _get_cryptography_public_key_info, +) + + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + 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 + +SIGNATURE_TEST_DATA = b'1234' + + +def _get_cryptography_private_key_info(key, need_private_key_data=False): + key_type, key_public_data = _get_cryptography_public_key_info(key.public_key()) + key_private_data = dict() + if need_private_key_data: + if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + private_numbers = key.private_numbers() + key_private_data['p'] = private_numbers.p + key_private_data['q'] = private_numbers.q + key_private_data['exponent'] = private_numbers.d + elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): + private_numbers = key.private_numbers() + key_private_data['x'] = private_numbers.x + elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): + private_numbers = key.private_numbers() + key_private_data['multiplier'] = private_numbers.private_value + 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 binary_exp_mod(g, q, p) != 1: + return False + # Check whether g**x mod p == y + if binary_exp_mod(g, x, p) != y: + return False + # Check (quickly) whether p or q are not primes + if quick_is_not_prime(q) or 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 PrivateKeyConsistencyError(OpenSSLObjectError): + def __init__(self, msg, result): + super(PrivateKeyConsistencyError, self).__init__(msg) + self.error_message = msg + self.result = result + + +class PrivateKeyParseError(OpenSSLObjectError): + def __init__(self, msg, result): + super(PrivateKeyParseError, self).__init__(msg) + self.error_message = msg + self.result = result + + +@six.add_metaclass(abc.ABCMeta) +class PrivateKeyInfoRetrieval(object): + def __init__(self, module, backend, content, passphrase=None, return_private_key_data=False, check_consistency=False): + # content must be a bytes string + self.module = module + self.backend = backend + self.content = content + self.passphrase = passphrase + self.return_private_key_data = return_private_key_data + self.check_consistency = check_consistency + + @abc.abstractmethod + def _get_public_key(self, binary): + pass + + @abc.abstractmethod + def _get_key_info(self, need_private_key_data=False): + pass + + @abc.abstractmethod + def _is_key_consistent(self, key_public_data, key_private_data): + pass + + def get_info(self, prefer_one_fingerprint=False): + result = dict( + can_parse_key=False, + key_is_consistent=None, + ) + priv_key_detail = self.content + try: + self.key = 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 OpenSSLObjectError as exc: + raise PrivateKeyParseError(to_native(exc), result) + + result['public_key'] = to_native(self._get_public_key(binary=False)) + pk = self._get_public_key(binary=True) + result['public_key_fingerprints'] = get_fingerprint_of_bytes( + pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict() + + key_type, key_public_data, key_private_data = self._get_key_info( + need_private_key_data=self.return_private_key_data or self.check_consistency) + result['type'] = key_type + result['public_data'] = key_public_data + if self.return_private_key_data: + result['private_data'] = key_private_data + + if self.check_consistency: + 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 do not know") + msg = ( + "Private key is not consistent! (See " + "https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)" + ) + raise PrivateKeyConsistencyError(msg, result) + return result + + +class PrivateKeyInfoRetrievalCryptography(PrivateKeyInfoRetrieval): + """Validate the supplied private key, using the cryptography backend""" + def __init__(self, module, content, **kwargs): + super(PrivateKeyInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, **kwargs) + + 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, need_private_key_data=False): + return _get_cryptography_private_key_info(self.key, need_private_key_data=need_private_key_data) + + def _is_key_consistent(self, key_public_data, key_private_data): + return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data) + + +def get_privatekey_info(module, backend, content, passphrase=None, return_private_key_data=False, prefer_one_fingerprint=False): + if backend == 'cryptography': + info = PrivateKeyInfoRetrievalCryptography( + module, content, passphrase=passphrase, return_private_key_data=return_private_key_data) + return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint) + + +def select_backend(module, backend, content, passphrase=None, return_private_key_data=False, check_consistency=False): + if backend == 'auto': + # Detection what is possible + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + + # Try cryptography + if can_use_cryptography: + backend = 'cryptography' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Cannot detect the required Python library " + "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) + + if backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + return backend, PrivateKeyInfoRetrievalCryptography( + module, content, passphrase=passphrase, return_private_key_data=return_private_key_data, check_consistency=check_consistency) + else: + raise ValueError('Unsupported value for backend: {0}'.format(backend)) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/publickey_info.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/publickey_info.py new file mode 100644 index 000000000..d381d2062 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/publickey_info.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2020-2021, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc +import traceback + +from ansible.module_utils import six +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + CRYPTOGRAPHY_HAS_X25519, + CRYPTOGRAPHY_HAS_X448, + CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED448, + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + get_fingerprint_of_bytes, + load_publickey, +) + + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + 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 + + +def _get_cryptography_public_key_info(key): + key_public_data = dict() + if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey): + key_type = 'RSA' + public_numbers = key.public_numbers() + key_public_data['size'] = key.key_size + key_public_data['modulus'] = public_numbers.n + key_public_data['exponent'] = public_numbers.e + elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey): + key_type = 'DSA' + parameter_numbers = key.parameters().parameter_numbers() + public_numbers = key.public_numbers() + key_public_data['size'] = key.key_size + key_public_data['p'] = parameter_numbers.p + key_public_data['q'] = parameter_numbers.q + key_public_data['g'] = parameter_numbers.g + key_public_data['y'] = public_numbers.y + elif CRYPTOGRAPHY_HAS_X25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey): + key_type = 'X25519' + elif CRYPTOGRAPHY_HAS_X448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey): + key_type = 'X448' + elif CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey): + key_type = 'Ed25519' + elif CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey): + key_type = 'Ed448' + elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey): + key_type = 'ECC' + public_numbers = key.public_numbers() + key_public_data['curve'] = key.curve.name + key_public_data['x'] = public_numbers.x + key_public_data['y'] = public_numbers.y + key_public_data['exponent_size'] = key.curve.key_size + else: + key_type = 'unknown ({0})'.format(type(key)) + return key_type, key_public_data + + +class PublicKeyParseError(OpenSSLObjectError): + def __init__(self, msg, result): + super(PublicKeyParseError, self).__init__(msg) + self.error_message = msg + self.result = result + + +@six.add_metaclass(abc.ABCMeta) +class PublicKeyInfoRetrieval(object): + def __init__(self, module, backend, content=None, key=None): + # content must be a bytes string + self.module = module + self.backend = backend + self.content = content + self.key = key + + @abc.abstractmethod + def _get_public_key(self, binary): + pass + + @abc.abstractmethod + def _get_key_info(self): + pass + + def get_info(self, prefer_one_fingerprint=False): + result = dict() + if self.key is None: + try: + self.key = load_publickey(content=self.content, backend=self.backend) + except OpenSSLObjectError as e: + raise PublicKeyParseError(to_native(e), {}) + + pk = self._get_public_key(binary=True) + result['fingerprints'] = get_fingerprint_of_bytes( + pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict() + + key_type, key_public_data = self._get_key_info() + result['type'] = key_type + result['public_data'] = key_public_data + return result + + +class PublicKeyInfoRetrievalCryptography(PublicKeyInfoRetrieval): + """Validate the supplied public key, using the cryptography backend""" + def __init__(self, module, content=None, key=None): + super(PublicKeyInfoRetrievalCryptography, self).__init__(module, 'cryptography', content=content, key=key) + + def _get_public_key(self, binary): + return self.key.public_bytes( + serialization.Encoding.DER if binary else serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo + ) + + def _get_key_info(self): + return _get_cryptography_public_key_info(self.key) + + +def get_publickey_info(module, backend, content=None, key=None, prefer_one_fingerprint=False): + if backend == 'cryptography': + info = PublicKeyInfoRetrievalCryptography(module, content=content, key=key) + return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint) + + +def select_backend(module, backend, content=None, key=None): + if backend == 'auto': + # Detection what is possible + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + + # Try cryptography + if can_use_cryptography: + backend = 'cryptography' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Cannot detect any of the required Python libraries " + "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) + + if backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + return backend, PublicKeyInfoRetrievalCryptography(module, content=content, key=key) + else: + raise ValueError('Unsupported value for backend: {0}'.format(backend)) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/openssh.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/openssh.py new file mode 100644 index 000000000..982475385 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/openssh.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2020, Doug Stanley <doug+ansible@technologixllc.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +# This import is only to maintain backwards compatibility +from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import ( # noqa: F401, pylint: disable=unused-import + parse_openssh_version +) diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/pem.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/pem.py new file mode 100644 index 000000000..4dc9745fe --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/pem.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +PEM_START = '-----BEGIN ' +PEM_END = '-----' +PKCS8_PRIVATEKEY_NAMES = ('PRIVATE KEY', 'ENCRYPTED PRIVATE KEY') +PKCS1_PRIVATEKEY_SUFFIX = ' PRIVATE KEY' + + +def identify_pem_format(content): + '''Given the contents of a binary file, tests whether this could be a PEM file.''' + try: + lines = content.decode('utf-8').splitlines(False) + if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END): + return True + except UnicodeDecodeError: + pass + return False + + +def identify_private_key_format(content): + '''Given the contents of a private key file, identifies its format.''' + # See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85 + # (PEM_read_bio_PrivateKey) + # and https://github.com/openssl/openssl/blob/master/include/openssl/pem.h#L46-L47 + # (PEM_STRING_PKCS8, PEM_STRING_PKCS8INF) + try: + lines = content.decode('utf-8').splitlines(False) + if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END): + name = lines[0][len(PEM_START):-len(PEM_END)] + if name in PKCS8_PRIVATEKEY_NAMES: + return 'pkcs8' + if len(name) > len(PKCS1_PRIVATEKEY_SUFFIX) and name.endswith(PKCS1_PRIVATEKEY_SUFFIX): + return 'pkcs1' + return 'unknown-pem' + except UnicodeDecodeError: + pass + return 'raw' + + +def split_pem_list(text, keep_inbetween=False): + ''' + Split concatenated PEM objects into a list of strings, where each is one PEM object. + ''' + result = [] + current = [] if keep_inbetween else None + for line in text.splitlines(True): + if line.strip(): + if not keep_inbetween and line.startswith('-----BEGIN '): + current = [] + if current is not None: + current.append(line) + if line.startswith('-----END '): + result.append(''.join(current)) + current = [] if keep_inbetween else None + return result + + +def extract_first_pem(text): + ''' + Given one PEM or multiple concatenated PEM objects, return only the first one, or None if there is none. + ''' + all_pems = split_pem_list(text) + if not all_pems: + return None + return all_pems[0] diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/support.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/support.py new file mode 100644 index 000000000..473246b1b --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/support.py @@ -0,0 +1,414 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc +import datetime +import errno +import hashlib +import os +import re + +from ansible.module_utils import six +from ansible.module_utils.common.text.converters import to_native, to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + identify_pem_format, +) + +try: + from OpenSSL import crypto + HAS_PYOPENSSL = True +except (ImportError, AttributeError): + # Error handled in the calling module. + HAS_PYOPENSSL = False + +try: + from cryptography import x509 + from cryptography.hazmat.backends import default_backend as cryptography_backend + from cryptography.hazmat.primitives.serialization import load_pem_private_key + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives import serialization +except ImportError: + # Error handled in the calling module. + pass + +from .basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + + +# This list of preferred fingerprints is used when prefer_one=True is supplied to the +# fingerprinting methods. +PREFERRED_FINGERPRINTS = ( + 'sha256', 'sha3_256', 'sha512', 'sha3_512', 'sha384', 'sha3_384', 'sha1', 'md5' +) + + +def get_fingerprint_of_bytes(source, prefer_one=False): + """Generate the fingerprint of the given bytes.""" + + fingerprint = {} + + try: + algorithms = hashlib.algorithms + except AttributeError: + try: + algorithms = hashlib.algorithms_guaranteed + except AttributeError: + return None + + if prefer_one: + # Sort algorithms to have the ones in PREFERRED_FINGERPRINTS at the beginning + prefered_algorithms = [algorithm for algorithm in PREFERRED_FINGERPRINTS if algorithm in algorithms] + prefered_algorithms += sorted([algorithm for algorithm in algorithms if algorithm not in PREFERRED_FINGERPRINTS]) + algorithms = prefered_algorithms + + for algo in algorithms: + f = getattr(hashlib, algo) + try: + h = f(source) + except ValueError: + # This can happen for hash algorithms not supported in FIPS mode + # (https://github.com/ansible/ansible/issues/67213) + continue + try: + # Certain hash functions have a hexdigest() which expects a length parameter + pubkey_digest = h.hexdigest() + except TypeError: + pubkey_digest = h.hexdigest(32) + fingerprint[algo] = ':'.join(pubkey_digest[i:i + 2] for i in range(0, len(pubkey_digest), 2)) + if prefer_one: + break + + return fingerprint + + +def get_fingerprint_of_privatekey(privatekey, backend='cryptography', prefer_one=False): + """Generate the fingerprint of the public key. """ + + if backend == 'cryptography': + publickey = privatekey.public_key().public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo + ) + + return get_fingerprint_of_bytes(publickey, prefer_one=prefer_one) + + +def get_fingerprint(path, passphrase=None, content=None, backend='cryptography', prefer_one=False): + """Generate the fingerprint of the public key. """ + + privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend) + + return get_fingerprint_of_privatekey(privatekey, backend=backend, prefer_one=prefer_one) + + +def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='cryptography'): + """Load the specified OpenSSL private key. + + The content can also be specified via content; in that case, + this function will not load the key from disk. + """ + + try: + if content is None: + with open(path, 'rb') as b_priv_key_fh: + priv_key_detail = b_priv_key_fh.read() + else: + priv_key_detail = content + except (IOError, OSError) as exc: + raise OpenSSLObjectError(exc) + + if backend == 'pyopenssl': + + # First try: try to load with real passphrase (resp. empty string) + # Will work if this is the correct passphrase, or the key is not + # password-protected. + try: + result = crypto.load_privatekey(crypto.FILETYPE_PEM, + priv_key_detail, + to_bytes(passphrase or '')) + except crypto.Error as e: + if len(e.args) > 0 and len(e.args[0]) > 0: + if e.args[0][0][2] in ('bad decrypt', 'bad password read'): + # This happens in case we have the wrong passphrase. + if passphrase is not None: + raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key!') + else: + raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!') + raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e)) + if check_passphrase: + # Next we want to make sure that the key is actually protected by + # a passphrase (in case we did try the empty string before, make + # sure that the key is not protected by the empty string) + try: + crypto.load_privatekey(crypto.FILETYPE_PEM, + priv_key_detail, + to_bytes('y' if passphrase == 'x' else 'x')) + if passphrase is not None: + # Since we can load the key without an exception, the + # key is not password-protected + raise OpenSSLBadPassphraseError('Passphrase provided, but private key is not password-protected!') + except crypto.Error as e: + if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0: + if e.args[0][0][2] in ('bad decrypt', 'bad password read'): + # The key is obviously protected by the empty string. + # Do not do this at home (if it's possible at all)... + raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!') + elif backend == 'cryptography': + try: + result = load_pem_private_key(priv_key_detail, + None if passphrase is None else to_bytes(passphrase), + cryptography_backend()) + except TypeError: + raise OpenSSLBadPassphraseError('Wrong or empty passphrase provided for private key') + except ValueError: + raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key') + + return result + + +def load_publickey(path=None, content=None, backend=None): + if content is None: + if path is None: + raise OpenSSLObjectError('Must provide either path or content') + try: + with open(path, 'rb') as b_priv_key_fh: + content = b_priv_key_fh.read() + except (IOError, OSError) as exc: + raise OpenSSLObjectError(exc) + + if backend == 'cryptography': + try: + return serialization.load_pem_public_key(content, backend=cryptography_backend()) + except Exception as e: + raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e)) + + +def load_certificate(path, content=None, backend='cryptography', der_support_enabled=False): + """Load the specified certificate.""" + + try: + if content is None: + with open(path, 'rb') as cert_fh: + cert_content = cert_fh.read() + else: + cert_content = content + except (IOError, OSError) as exc: + raise OpenSSLObjectError(exc) + if backend == 'pyopenssl': + if der_support_enabled is False or identify_pem_format(cert_content): + return crypto.load_certificate(crypto.FILETYPE_PEM, cert_content) + elif der_support_enabled: + raise OpenSSLObjectError('Certificate in DER format is not supported by the pyopenssl backend.') + elif backend == 'cryptography': + if der_support_enabled is False or identify_pem_format(cert_content): + try: + return x509.load_pem_x509_certificate(cert_content, cryptography_backend()) + except ValueError as exc: + raise OpenSSLObjectError(exc) + elif der_support_enabled: + try: + return x509.load_der_x509_certificate(cert_content, cryptography_backend()) + except ValueError as exc: + raise OpenSSLObjectError('Cannot parse DER certificate: {0}'.format(exc)) + + +def load_certificate_request(path, content=None, backend='cryptography'): + """Load the specified certificate signing request.""" + try: + if content is None: + with open(path, 'rb') as csr_fh: + csr_content = csr_fh.read() + else: + csr_content = content + except (IOError, OSError) as exc: + raise OpenSSLObjectError(exc) + if backend == 'cryptography': + try: + return x509.load_pem_x509_csr(csr_content, cryptography_backend()) + except ValueError as exc: + raise OpenSSLObjectError(exc) + + +def parse_name_field(input_dict, name_field_name=None): + """Take a dict with key: value or key: list_of_values mappings and return a list of tuples""" + error_str = '{key}' if name_field_name is None else '{key} in {name}' + + result = [] + for key, value in input_dict.items(): + if isinstance(value, list): + for entry in value: + if not isinstance(entry, six.string_types): + raise TypeError(('Values %s must be strings' % error_str).format(key=key, name=name_field_name)) + if not entry: + raise ValueError(('Values for %s must not be empty strings' % error_str).format(key=key)) + result.append((key, entry)) + elif isinstance(value, six.string_types): + if not value: + raise ValueError(('Value for %s must not be an empty string' % error_str).format(key=key)) + result.append((key, value)) + else: + raise TypeError(('Value for %s must be either a string or a list of strings' % error_str).format(key=key)) + return result + + +def parse_ordered_name_field(input_list, name_field_name): + """Take a dict with key: value or key: list_of_values mappings and return a list of tuples""" + + result = [] + for index, entry in enumerate(input_list): + if len(entry) != 1: + raise ValueError( + 'Entry #{index} in {name} must be a dictionary with exactly one key-value pair'.format( + name=name_field_name, index=index + 1)) + try: + result.extend(parse_name_field(entry, name_field_name=name_field_name)) + except (TypeError, ValueError) as exc: + raise ValueError( + 'Error while processing entry #{index} in {name}: {error}'.format( + name=name_field_name, index=index + 1, error=exc)) + return result + + +def convert_relative_to_datetime(relative_time_string): + """Get a datetime.datetime or None from a string in the time format described in sshd_config(5)""" + + parsed_result = re.match( + r"^(?P<prefix>[+-])((?P<weeks>\d+)[wW])?((?P<days>\d+)[dD])?((?P<hours>\d+)[hH])?((?P<minutes>\d+)[mM])?((?P<seconds>\d+)[sS]?)?$", + relative_time_string) + + if parsed_result is None or len(relative_time_string) == 1: + # not matched or only a single "+" or "-" + return None + + offset = datetime.timedelta(0) + if parsed_result.group("weeks") is not None: + offset += datetime.timedelta(weeks=int(parsed_result.group("weeks"))) + if parsed_result.group("days") is not None: + offset += datetime.timedelta(days=int(parsed_result.group("days"))) + if parsed_result.group("hours") is not None: + offset += datetime.timedelta(hours=int(parsed_result.group("hours"))) + if parsed_result.group("minutes") is not None: + offset += datetime.timedelta( + minutes=int(parsed_result.group("minutes"))) + if parsed_result.group("seconds") is not None: + offset += datetime.timedelta( + seconds=int(parsed_result.group("seconds"))) + + if parsed_result.group("prefix") == "+": + return datetime.datetime.utcnow() + offset + else: + return datetime.datetime.utcnow() - offset + + +def get_relative_time_option(input_string, input_name, backend='cryptography'): + """Return an absolute timespec if a relative timespec or an ASN1 formatted + string is provided. + + The return value will be a datetime object for the cryptography backend, + and a ASN1 formatted string for the pyopenssl backend.""" + result = to_native(input_string) + if result is None: + raise OpenSSLObjectError( + 'The timespec "%s" for %s is not valid' % + input_string, input_name) + # Relative time + if result.startswith("+") or result.startswith("-"): + result_datetime = convert_relative_to_datetime(result) + if backend == 'pyopenssl': + return result_datetime.strftime("%Y%m%d%H%M%SZ") + elif backend == 'cryptography': + return result_datetime + # Absolute time + if backend == 'cryptography': + for date_fmt in ['%Y%m%d%H%M%SZ', '%Y%m%d%H%MZ', '%Y%m%d%H%M%S%z', '%Y%m%d%H%M%z']: + try: + return datetime.datetime.strptime(result, date_fmt) + except ValueError: + pass + + raise OpenSSLObjectError( + 'The time spec "%s" for %s is invalid' % + (input_string, input_name) + ) + + +def select_message_digest(digest_string): + digest = None + if digest_string == 'sha256': + digest = hashes.SHA256() + elif digest_string == 'sha384': + digest = hashes.SHA384() + elif digest_string == 'sha512': + digest = hashes.SHA512() + elif digest_string == 'sha1': + digest = hashes.SHA1() + elif digest_string == 'md5': + digest = hashes.MD5() + return digest + + +@six.add_metaclass(abc.ABCMeta) +class OpenSSLObject(object): + + def __init__(self, path, state, force, check_mode): + self.path = path + self.state = state + self.force = force + self.name = os.path.basename(path) + self.changed = False + self.check_mode = check_mode + + def check(self, module, perms_required=True): + """Ensure the resource is in its desired state.""" + + def _check_state(): + return os.path.exists(self.path) + + def _check_perms(module): + file_args = module.load_file_common_arguments(module.params) + if module.check_file_absent_if_check_mode(file_args['path']): + return False + return not module.set_fs_attributes_if_different(file_args, False) + + if not perms_required: + return _check_state() + + return _check_state() and _check_perms(module) + + @abc.abstractmethod + def dump(self): + """Serialize the object into a dictionary.""" + + pass + + @abc.abstractmethod + def generate(self): + """Generate the resource.""" + + pass + + def remove(self, module): + """Remove the resource from the filesystem.""" + if self.check_mode: + if os.path.exists(self.path): + self.changed = True + return + + try: + os.remove(self.path) + self.changed = True + except OSError as exc: + if exc.errno != errno.ENOENT: + raise OpenSSLObjectError(exc) + else: + pass diff --git a/ansible_collections/community/crypto/plugins/module_utils/ecs/api.py b/ansible_collections/community/crypto/plugins/module_utils/ecs/api.py new file mode 100644 index 000000000..bf8be58f0 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/ecs/api.py @@ -0,0 +1,346 @@ +# -*- 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/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json +import os +import re +import traceback + +from ansible.module_utils.common.text.converters 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 do not 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() diff --git a/ansible_collections/community/crypto/plugins/module_utils/io.py b/ansible_collections/community/crypto/plugins/module_utils/io.py new file mode 100644 index 000000000..6c2f33be7 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/io.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import errno +import os +import tempfile + + +def load_file(path, module=None): + ''' + Load the file as a bytes string. + ''' + try: + with open(path, 'rb') as f: + return f.read() + except Exception as exc: + if module is None: + raise + module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) + + +def load_file_if_exists(path, module=None, ignore_errors=False): + ''' + Load the file as a bytes string. If the file does not exist, ``None`` is returned. + + If ``ignore_errors`` is ``True``, will ignore errors. Otherwise, errors are + raised as exceptions if ``module`` is not specified, and result in ``module.fail_json`` + being called when ``module`` is specified. + ''' + try: + with open(path, 'rb') as f: + return f.read() + except EnvironmentError as exc: + if exc.errno == errno.ENOENT: + return None + if ignore_errors: + return None + if module is None: + raise + module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) + except Exception as exc: + if ignore_errors: + return None + if module is None: + raise + module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) + + +def write_file(module, content, default_mode=None, path=None): + ''' + Writes content into destination file as securely as possible. + Uses file arguments from module. + ''' + # Find out parameters for file + try: + file_args = module.load_file_common_arguments(module.params, path=path) + except TypeError: + # The path argument is only supported in Ansible 2.10+. Fall back to + # pre-2.10 behavior of module_utils/crypto.py for older Ansible versions. + file_args = module.load_file_common_arguments(module.params) + if path is not None: + file_args['path'] = path + if file_args['mode'] is None: + file_args['mode'] = default_mode + # Create tempfile name + tmp_fd, tmp_name = tempfile.mkstemp(prefix=b'.ansible_tmp') + try: + os.close(tmp_fd) + except Exception: + pass + module.add_cleanup_file(tmp_name) # if we fail, let Ansible try to remove the file + try: + try: + # Create tempfile + file = os.open(tmp_name, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + os.write(file, content) + os.close(file) + except Exception as e: + try: + os.remove(tmp_name) + except Exception: + pass + module.fail_json(msg='Error while writing result into temporary file: {0}'.format(e)) + # Update destination to wanted permissions + if os.path.exists(file_args['path']): + module.set_fs_attributes_if_different(file_args, False) + # Move tempfile to final destination + module.atomic_move(tmp_name, file_args['path']) + # Try to update permissions again + if not module.check_file_absent_if_check_mode(file_args['path']): + module.set_fs_attributes_if_different(file_args, False) + except Exception as e: + try: + os.remove(tmp_name) + except Exception: + pass + module.fail_json(msg='Error while writing result: {0}'.format(e)) diff --git a/ansible_collections/community/crypto/plugins/module_utils/openssh/backends/common.py b/ansible_collections/community/crypto/plugins/module_utils/openssh/backends/common.py new file mode 100644 index 000000000..6e274a6de --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/openssh/backends/common.py @@ -0,0 +1,346 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import abc +import os +import stat +import traceback + +from ansible.module_utils import six + +from ansible.module_utils.common.text.converters import to_native +from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import ( + parse_openssh_version, +) + + +def restore_on_failure(f): + def backup_and_restore(module, path, *args, **kwargs): + backup_file = module.backup_local(path) if os.path.exists(path) else None + + try: + f(module, path, *args, **kwargs) + except Exception: + if backup_file is not None: + module.atomic_move(backup_file, path) + raise + else: + module.add_cleanup_file(backup_file) + + return backup_and_restore + + +@restore_on_failure +def safe_atomic_move(module, path, destination): + module.atomic_move(path, destination) + + +def _restore_all_on_failure(f): + def backup_and_restore(self, sources_and_destinations, *args, **kwargs): + backups = [(d, self.module.backup_local(d)) for s, d in sources_and_destinations if os.path.exists(d)] + + try: + f(self, sources_and_destinations, *args, **kwargs) + except Exception: + for destination, backup in backups: + self.module.atomic_move(backup, destination) + raise + else: + for destination, backup in backups: + self.module.add_cleanup_file(backup) + return backup_and_restore + + +@six.add_metaclass(abc.ABCMeta) +class OpensshModule(object): + def __init__(self, module): + self.module = module + + self.changed = False + self.check_mode = self.module.check_mode + + def execute(self): + try: + self._execute() + except Exception as e: + self.module.fail_json( + msg="unexpected error occurred: %s" % to_native(e), + exception=traceback.format_exc(), + ) + + self.module.exit_json(**self.result) + + @abc.abstractmethod + def _execute(self): + pass + + @property + def result(self): + result = self._result + + result['changed'] = self.changed + + if self.module._diff: + result['diff'] = self.diff + + return result + + @property + @abc.abstractmethod + def _result(self): + pass + + @property + @abc.abstractmethod + def diff(self): + pass + + @staticmethod + def skip_if_check_mode(f): + def wrapper(self, *args, **kwargs): + if not self.check_mode: + f(self, *args, **kwargs) + return wrapper + + @staticmethod + def trigger_change(f): + def wrapper(self, *args, **kwargs): + f(self, *args, **kwargs) + self.changed = True + return wrapper + + def _check_if_base_dir(self, path): + base_dir = os.path.dirname(path) or '.' + if not os.path.isdir(base_dir): + self.module.fail_json( + name=base_dir, + msg='The directory %s does not exist or the file is not a directory' % base_dir + ) + + def _get_ssh_version(self): + ssh_bin = self.module.get_bin_path('ssh') + if not ssh_bin: + return "" + return parse_openssh_version(self.module.run_command([ssh_bin, '-V', '-q'])[2].strip()) + + @_restore_all_on_failure + def _safe_secure_move(self, sources_and_destinations): + """Moves a list of files from 'source' to 'destination' and restores 'destination' from backup upon failure. + If 'destination' does not already exist, then 'source' permissions are preserved to prevent + exposing protected data ('atomic_move' uses the 'destination' base directory mask for + permissions if 'destination' does not already exists). + """ + for source, destination in sources_and_destinations: + if os.path.exists(destination): + self.module.atomic_move(source, destination) + else: + self.module.preserved_copy(source, destination) + + def _update_permissions(self, path): + file_args = self.module.load_file_common_arguments(self.module.params) + file_args['path'] = path + + if not self.module.check_file_absent_if_check_mode(path): + self.changed = self.module.set_fs_attributes_if_different(file_args, self.changed) + else: + self.changed = True + + +class KeygenCommand(object): + def __init__(self, module): + self._bin_path = module.get_bin_path('ssh-keygen', True) + self._run_command = module.run_command + + def generate_certificate(self, certificate_path, identifier, options, pkcs11_provider, principals, + serial_number, signature_algorithm, signing_key_path, type, + time_parameters, use_agent, **kwargs): + args = [self._bin_path, '-s', signing_key_path, '-P', '', '-I', identifier] + + if options: + for option in options: + args.extend(['-O', option]) + if pkcs11_provider: + args.extend(['-D', pkcs11_provider]) + if principals: + args.extend(['-n', ','.join(principals)]) + if serial_number is not None: + args.extend(['-z', str(serial_number)]) + if type == 'host': + args.extend(['-h']) + if use_agent: + args.extend(['-U']) + if time_parameters.validity_string: + args.extend(['-V', time_parameters.validity_string]) + if signature_algorithm: + args.extend(['-t', signature_algorithm]) + args.append(certificate_path) + + return self._run_command(args, **kwargs) + + def generate_keypair(self, private_key_path, size, type, comment, **kwargs): + args = [ + self._bin_path, + '-q', + '-N', '', + '-b', str(size), + '-t', type, + '-f', private_key_path, + '-C', comment or '' + ] + + # "y" must be entered in response to the "overwrite" prompt + data = 'y' if os.path.exists(private_key_path) else None + + return self._run_command(args, data=data, **kwargs) + + def get_certificate_info(self, certificate_path, **kwargs): + return self._run_command([self._bin_path, '-L', '-f', certificate_path], **kwargs) + + def get_matching_public_key(self, private_key_path, **kwargs): + return self._run_command([self._bin_path, '-P', '', '-y', '-f', private_key_path], **kwargs) + + def get_private_key(self, private_key_path, **kwargs): + return self._run_command([self._bin_path, '-l', '-f', private_key_path], **kwargs) + + def update_comment(self, private_key_path, comment, **kwargs): + if os.path.exists(private_key_path) and not os.access(private_key_path, os.W_OK): + try: + os.chmod(private_key_path, stat.S_IWUSR + stat.S_IRUSR) + except (IOError, OSError) as e: + raise e("The private key at %s is not writeable preventing a comment update" % private_key_path) + + return self._run_command([self._bin_path, '-q', '-o', '-c', '-C', comment, '-f', private_key_path], **kwargs) + + +class PrivateKey(object): + def __init__(self, size, key_type, fingerprint, format=''): + self._size = size + self._type = key_type + self._fingerprint = fingerprint + self._format = format + + @property + def size(self): + return self._size + + @property + def type(self): + return self._type + + @property + def fingerprint(self): + return self._fingerprint + + @property + def format(self): + return self._format + + @classmethod + def from_string(cls, string): + properties = string.split() + + return cls( + size=int(properties[0]), + key_type=properties[-1][1:-1].lower(), + fingerprint=properties[1], + ) + + def to_dict(self): + return { + 'size': self._size, + 'type': self._type, + 'fingerprint': self._fingerprint, + 'format': self._format, + } + + +class PublicKey(object): + def __init__(self, type_string, data, comment): + self._type_string = type_string + self._data = data + self._comment = comment + + def __eq__(self, other): + if not isinstance(other, type(self)): + return NotImplemented + + return all([ + self._type_string == other._type_string, + self._data == other._data, + (self._comment == other._comment) if self._comment is not None and other._comment is not None else True + ]) + + def __ne__(self, other): + return not self == other + + def __str__(self): + return "%s %s" % (self._type_string, self._data) + + @property + def comment(self): + return self._comment + + @comment.setter + def comment(self, value): + self._comment = value + + @property + def data(self): + return self._data + + @property + def type_string(self): + return self._type_string + + @classmethod + def from_string(cls, string): + properties = string.strip('\n').split(' ', 2) + + return cls( + type_string=properties[0], + data=properties[1], + comment=properties[2] if len(properties) > 2 else "" + ) + + @classmethod + def load(cls, path): + try: + with open(path, 'r') as f: + properties = f.read().strip(' \n').split(' ', 2) + except (IOError, OSError): + raise + + if len(properties) < 2: + return None + + return cls( + type_string=properties[0], + data=properties[1], + comment='' if len(properties) <= 2 else properties[2], + ) + + def to_dict(self): + return { + 'comment': self._comment, + 'public_key': self._data, + } + + +def parse_private_key_format(path): + with open(path, 'r') as file: + header = file.readline().strip() + + if header == '-----BEGIN OPENSSH PRIVATE KEY-----': + return 'SSH' + elif header == '-----BEGIN PRIVATE KEY-----': + return 'PKCS8' + elif header == '-----BEGIN RSA PRIVATE KEY-----': + return 'PKCS1' + + return '' diff --git a/ansible_collections/community/crypto/plugins/module_utils/openssh/backends/keypair_backend.py b/ansible_collections/community/crypto/plugins/module_utils/openssh/backends/keypair_backend.py new file mode 100644 index 000000000..e3bc3535b --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/openssh/backends/keypair_backend.py @@ -0,0 +1,482 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at> +# Copyright (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import abc +import os + +from ansible.module_utils import six +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.openssh.cryptography import ( + HAS_OPENSSH_SUPPORT, + HAS_OPENSSH_PRIVATE_FORMAT, + InvalidCommentError, + InvalidPassphraseError, + InvalidPrivateKeyFileError, + OpenSSHError, + OpensshKeypair, +) +from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import ( + KeygenCommand, + OpensshModule, + PrivateKey, + PublicKey, + parse_private_key_format, +) +from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import ( + any_in, + file_mode, + secure_write, +) + + +@six.add_metaclass(abc.ABCMeta) +class KeypairBackend(OpensshModule): + + def __init__(self, module): + super(KeypairBackend, self).__init__(module) + + self.comment = self.module.params['comment'] + self.private_key_path = self.module.params['path'] + self.public_key_path = self.private_key_path + '.pub' + self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always' + self.state = self.module.params['state'] + self.type = self.module.params['type'] + + self.size = self._get_size(self.module.params['size']) + self._validate_path() + + self.original_private_key = None + self.original_public_key = None + self.private_key = None + self.public_key = None + + def _get_size(self, size): + if self.type in ('rsa', 'rsa1'): + result = 4096 if size is None else size + if result < 1024: + return self.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." + ) + elif self.type == 'dsa': + result = 1024 if size is None else size + if result != 1024: + return self.module.fail_json(msg="DSA keys must be exactly 1024 bits as specified by FIPS 186-2.") + elif self.type == 'ecdsa': + result = 256 if size is None else size + if result not in (256, 384, 521): + return self.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." + ) + elif self.type == 'ed25519': + # User input is ignored for `key size` when `key type` is ed25519 + result = 256 + else: + return self.module.fail_json(msg="%s is not a valid value for key type" % self.type) + + return result + + def _validate_path(self): + self._check_if_base_dir(self.private_key_path) + + if os.path.isdir(self.private_key_path): + self.module.fail_json(msg='%s is a directory. Please specify a path to a file.' % self.private_key_path) + + def _execute(self): + self.original_private_key = self._load_private_key() + self.original_public_key = self._load_public_key() + + if self.state == 'present': + self._validate_key_load() + + if self._should_generate(): + self._generate() + elif not self._public_key_valid(): + self._restore_public_key() + + self.private_key = self._load_private_key() + self.public_key = self._load_public_key() + + for path in (self.private_key_path, self.public_key_path): + self._update_permissions(path) + else: + if self._should_remove(): + self._remove() + + def _load_private_key(self): + result = None + if self._private_key_exists(): + try: + result = self._get_private_key() + except Exception: + pass + + return result + + def _private_key_exists(self): + return os.path.exists(self.private_key_path) + + @abc.abstractmethod + def _get_private_key(self): + pass + + def _load_public_key(self): + result = None + if self._public_key_exists(): + try: + result = PublicKey.load(self.public_key_path) + except (IOError, OSError): + pass + return result + + def _public_key_exists(self): + return os.path.exists(self.public_key_path) + + def _validate_key_load(self): + if (self._private_key_exists() + and self.regenerate in ('never', 'fail', 'partial_idempotence') + and (self.original_private_key is None or not self._private_key_readable())): + self.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=true`." + ) + + @abc.abstractmethod + def _private_key_readable(self): + pass + + def _should_generate(self): + if self.original_private_key is None: + return True + elif self.regenerate == 'never': + return False + elif self.regenerate == 'fail': + if not self._private_key_valid(): + self.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=true`." + ) + return False + elif self.regenerate in ('partial_idempotence', 'full_idempotence'): + return not self._private_key_valid() + else: + return True + + def _private_key_valid(self): + if self.original_private_key is None: + return False + + return all([ + self.size == self.original_private_key.size, + self.type == self.original_private_key.type, + self._private_key_valid_backend(), + ]) + + @abc.abstractmethod + def _private_key_valid_backend(self): + pass + + @OpensshModule.trigger_change + @OpensshModule.skip_if_check_mode + def _generate(self): + temp_private_key, temp_public_key = self._generate_temp_keypair() + + try: + self._safe_secure_move([(temp_private_key, self.private_key_path), (temp_public_key, self.public_key_path)]) + except OSError as e: + self.module.fail_json(msg=to_native(e)) + + def _generate_temp_keypair(self): + temp_private_key = os.path.join(self.module.tmpdir, os.path.basename(self.private_key_path)) + temp_public_key = temp_private_key + '.pub' + + try: + self._generate_keypair(temp_private_key) + except (IOError, OSError) as e: + self.module.fail_json(msg=to_native(e)) + + for f in (temp_private_key, temp_public_key): + self.module.add_cleanup_file(f) + + return temp_private_key, temp_public_key + + @abc.abstractmethod + def _generate_keypair(self, private_key_path): + pass + + def _public_key_valid(self): + if self.original_public_key is None: + return False + + valid_public_key = self._get_public_key() + valid_public_key.comment = self.comment + + return self.original_public_key == valid_public_key + + @abc.abstractmethod + def _get_public_key(self): + pass + + @OpensshModule.trigger_change + @OpensshModule.skip_if_check_mode + def _restore_public_key(self): + try: + temp_public_key = self._create_temp_public_key(str(self._get_public_key()) + '\n') + self._safe_secure_move([ + (temp_public_key, self.public_key_path) + ]) + except (IOError, OSError): + self.module.fail_json( + msg="The public key is missing or does not match the private key. " + + "Unable to regenerate the public key." + ) + + if self.comment: + self._update_comment() + + def _create_temp_public_key(self, content): + temp_public_key = os.path.join(self.module.tmpdir, os.path.basename(self.public_key_path)) + + default_permissions = 0o644 + existing_permissions = file_mode(self.public_key_path) + + try: + secure_write(temp_public_key, existing_permissions or default_permissions, to_bytes(content)) + except (IOError, OSError) as e: + self.module.fail_json(msg=to_native(e)) + self.module.add_cleanup_file(temp_public_key) + + return temp_public_key + + @abc.abstractmethod + def _update_comment(self): + pass + + def _should_remove(self): + return self._private_key_exists() or self._public_key_exists() + + @OpensshModule.trigger_change + @OpensshModule.skip_if_check_mode + def _remove(self): + try: + if self._private_key_exists(): + os.remove(self.private_key_path) + if self._public_key_exists(): + os.remove(self.public_key_path) + except (IOError, OSError) as e: + self.module.fail_json(msg=to_native(e)) + + @property + def _result(self): + private_key = self.private_key or self.original_private_key + public_key = self.public_key or self.original_public_key + + return { + 'size': self.size, + 'type': self.type, + 'filename': self.private_key_path, + 'fingerprint': private_key.fingerprint if private_key else '', + 'public_key': str(public_key) if public_key else '', + 'comment': public_key.comment if public_key else '', + } + + @property + def diff(self): + before = self.original_private_key.to_dict() if self.original_private_key else {} + before.update(self.original_public_key.to_dict() if self.original_public_key else {}) + + after = self.private_key.to_dict() if self.private_key else {} + after.update(self.public_key.to_dict() if self.public_key else {}) + + return { + 'before': before, + 'after': after, + } + + +class KeypairBackendOpensshBin(KeypairBackend): + def __init__(self, module): + super(KeypairBackendOpensshBin, self).__init__(module) + + if self.module.params['private_key_format'] != 'auto': + self.module.fail_json( + msg="'auto' is the only valid option for " + + "'private_key_format' when 'backend' is not 'cryptography'" + ) + + self.ssh_keygen = KeygenCommand(self.module) + + def _generate_keypair(self, private_key_path): + self.ssh_keygen.generate_keypair(private_key_path, self.size, self.type, self.comment) + + def _get_private_key(self): + private_key_content = self.ssh_keygen.get_private_key(self.private_key_path)[1] + return PrivateKey.from_string(private_key_content) + + def _get_public_key(self): + public_key_content = self.ssh_keygen.get_matching_public_key(self.private_key_path)[1] + return PublicKey.from_string(public_key_content) + + def _private_key_readable(self): + rc, stdout, stderr = self.ssh_keygen.get_matching_public_key(self.private_key_path) + return not (rc == 255 or any_in(stderr, 'is not a public key file', 'incorrect passphrase', 'load failed')) + + def _update_comment(self): + try: + self.ssh_keygen.update_comment(self.private_key_path, self.comment) + except (IOError, OSError) as e: + self.module.fail_json(msg=to_native(e)) + + def _private_key_valid_backend(self): + return True + + +class KeypairBackendCryptography(KeypairBackend): + def __init__(self, module): + super(KeypairBackendCryptography, self).__init__(module) + + if self.type == 'rsa1': + self.module.fail_json(msg="RSA1 keys are not supported by the cryptography backend") + + self.passphrase = to_bytes(module.params['passphrase']) if module.params['passphrase'] else None + self.private_key_format = self._get_key_format(module.params['private_key_format']) + + def _get_key_format(self, key_format): + result = 'SSH' + + if key_format == 'auto': + # Default to OpenSSH 7.8 compatibility when OpenSSH is not installed + ssh_version = self._get_ssh_version() or "7.8" + + if LooseVersion(ssh_version) < LooseVersion("7.8") and self.type != 'ed25519': + # OpenSSH made SSH formatted private keys available in version 6.5, + # but still defaulted to PKCS1 format with the exception of ed25519 keys + result = 'PKCS1' + + if result == 'SSH' and not HAS_OPENSSH_PRIVATE_FORMAT: + self.module.fail_json( + msg=missing_required_lib( + 'cryptography >= 3.0', + reason="to load/dump private keys in the default OpenSSH format for OpenSSH >= 7.8 " + + "or for ed25519 keys" + ) + ) + else: + result = key_format.upper() + + return result + + def _generate_keypair(self, private_key_path): + keypair = OpensshKeypair.generate( + keytype=self.type, + size=self.size, + passphrase=self.passphrase, + comment=self.comment or '', + ) + + encoded_private_key = OpensshKeypair.encode_openssh_privatekey( + keypair.asymmetric_keypair, self.private_key_format + ) + secure_write(private_key_path, 0o600, encoded_private_key) + + public_key_path = private_key_path + '.pub' + secure_write(public_key_path, 0o644, keypair.public_key) + + def _get_private_key(self): + keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True) + + return PrivateKey( + size=keypair.size, + key_type=keypair.key_type, + fingerprint=keypair.fingerprint, + format=parse_private_key_format(self.private_key_path) + ) + + def _get_public_key(self): + try: + keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True) + except OpenSSHError: + # Simulates the null output of ssh-keygen + return "" + + return PublicKey.from_string(to_text(keypair.public_key)) + + def _private_key_readable(self): + try: + OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True) + except (InvalidPrivateKeyFileError, InvalidPassphraseError): + return False + + # Cryptography >= 3.0 uses a SSH key loader which does not raise an exception when a passphrase is provided + # when loading an unencrypted key + if self.passphrase: + try: + OpensshKeypair.load(path=self.private_key_path, passphrase=None, no_public_key=True) + except (InvalidPrivateKeyFileError, InvalidPassphraseError): + return True + else: + return False + + return True + + def _update_comment(self): + keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True) + try: + keypair.comment = self.comment + except InvalidCommentError as e: + self.module.fail_json(msg=to_native(e)) + + try: + temp_public_key = self._create_temp_public_key(keypair.public_key + b'\n') + self._safe_secure_move([(temp_public_key, self.public_key_path)]) + except (IOError, OSError) as e: + self.module.fail_json(msg=to_native(e)) + + def _private_key_valid_backend(self): + # avoids breaking behavior and prevents + # automatic conversions with OpenSSH upgrades + if self.module.params['private_key_format'] == 'auto': + return True + + return self.private_key_format == self.original_private_key.format + + +def select_backend(module, backend): + can_use_cryptography = HAS_OPENSSH_SUPPORT + can_use_opensshbin = bool(module.get_bin_path('ssh-keygen')) + + if backend == 'auto': + if can_use_opensshbin and not module.params['passphrase']: + backend = 'opensshbin' + elif can_use_cryptography: + backend = 'cryptography' + else: + module.fail_json(msg="Cannot find either the OpenSSH binary in the PATH " + + "or cryptography >= 2.6 installed on this system") + + if backend == 'opensshbin': + if not can_use_opensshbin: + module.fail_json(msg="Cannot find the OpenSSH binary in the PATH") + return backend, KeypairBackendOpensshBin(module) + elif backend == 'cryptography': + if not can_use_cryptography: + module.fail_json(msg=missing_required_lib("cryptography >= 2.6")) + return backend, KeypairBackendCryptography(module) + else: + raise ValueError('Unsupported value for backend: {0}'.format(backend)) diff --git a/ansible_collections/community/crypto/plugins/module_utils/openssh/certificate.py b/ansible_collections/community/crypto/plugins/module_utils/openssh/certificate.py new file mode 100644 index 000000000..54d1b1ec5 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/openssh/certificate.py @@ -0,0 +1,666 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +# Protocol References +# ------------------- +# https://datatracker.ietf.org/doc/html/rfc4251 +# https://datatracker.ietf.org/doc/html/rfc4253 +# https://datatracker.ietf.org/doc/html/rfc5656 +# https://datatracker.ietf.org/doc/html/rfc8032 +# https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD +# +# Inspired by: +# ------------ +# https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/serialization/ssh.py +# https://github.com/paramiko/paramiko/blob/master/paramiko/message.py + +import abc +import binascii +import os +from base64 import b64encode +from datetime import datetime +from hashlib import sha256 + +from ansible.module_utils import six +from ansible.module_utils.common.text.converters import to_text +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import convert_relative_to_datetime +from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import ( + OpensshParser, + _OpensshWriter, +) + +# See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD +_USER_TYPE = 1 +_HOST_TYPE = 2 + +_SSH_TYPE_STRINGS = { + 'rsa': b"ssh-rsa", + 'dsa': b"ssh-dss", + 'ecdsa-nistp256': b"ecdsa-sha2-nistp256", + 'ecdsa-nistp384': b"ecdsa-sha2-nistp384", + 'ecdsa-nistp521': b"ecdsa-sha2-nistp521", + 'ed25519': b"ssh-ed25519", +} +_CERT_SUFFIX_V01 = b"-cert-v01@openssh.com" + +# See https://datatracker.ietf.org/doc/html/rfc5656#section-6.1 +_ECDSA_CURVE_IDENTIFIERS = { + 'ecdsa-nistp256': b'nistp256', + 'ecdsa-nistp384': b'nistp384', + 'ecdsa-nistp521': b'nistp521', +} +_ECDSA_CURVE_IDENTIFIERS_LOOKUP = { + b'nistp256': 'ecdsa-nistp256', + b'nistp384': 'ecdsa-nistp384', + b'nistp521': 'ecdsa-nistp521', +} + +_ALWAYS = datetime(1970, 1, 1) +_FOREVER = datetime.max + +_CRITICAL_OPTIONS = ( + 'force-command', + 'source-address', + 'verify-required', +) + +_DIRECTIVES = ( + 'clear', + 'no-x11-forwarding', + 'no-agent-forwarding', + 'no-port-forwarding', + 'no-pty', + 'no-user-rc', +) + +_EXTENSIONS = ( + 'permit-x11-forwarding', + 'permit-agent-forwarding', + 'permit-port-forwarding', + 'permit-pty', + 'permit-user-rc' +) + +if six.PY3: + long = int + + +class OpensshCertificateTimeParameters(object): + def __init__(self, valid_from, valid_to): + self._valid_from = self.to_datetime(valid_from) + self._valid_to = self.to_datetime(valid_to) + + if self._valid_from > self._valid_to: + raise ValueError("Valid from: %s must not be greater than Valid to: %s" % (valid_from, valid_to)) + + def __eq__(self, other): + if not isinstance(other, type(self)): + return NotImplemented + else: + return self._valid_from == other._valid_from and self._valid_to == other._valid_to + + def __ne__(self, other): + return not self == other + + @property + def validity_string(self): + if not (self._valid_from == _ALWAYS and self._valid_to == _FOREVER): + return "%s:%s" % ( + self.valid_from(date_format='openssh'), self.valid_to(date_format='openssh') + ) + return "" + + def valid_from(self, date_format): + return self.format_datetime(self._valid_from, date_format) + + def valid_to(self, date_format): + return self.format_datetime(self._valid_to, date_format) + + def within_range(self, valid_at): + if valid_at is not None: + valid_at_datetime = self.to_datetime(valid_at) + return self._valid_from <= valid_at_datetime <= self._valid_to + return True + + @staticmethod + def format_datetime(dt, date_format): + if date_format in ('human_readable', 'openssh'): + if dt == _ALWAYS: + result = 'always' + elif dt == _FOREVER: + result = 'forever' + else: + result = dt.isoformat() if date_format == 'human_readable' else dt.strftime("%Y%m%d%H%M%S") + elif date_format == 'timestamp': + td = dt - _ALWAYS + result = int((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6) + else: + raise ValueError("%s is not a valid format" % date_format) + return result + + @staticmethod + def to_datetime(time_string_or_timestamp): + try: + if isinstance(time_string_or_timestamp, six.string_types): + result = OpensshCertificateTimeParameters._time_string_to_datetime(time_string_or_timestamp.strip()) + elif isinstance(time_string_or_timestamp, (long, int)): + result = OpensshCertificateTimeParameters._timestamp_to_datetime(time_string_or_timestamp) + else: + raise ValueError( + "Value must be of type (str, unicode, int, long) not %s" % type(time_string_or_timestamp) + ) + except ValueError: + raise + return result + + @staticmethod + def _timestamp_to_datetime(timestamp): + if timestamp == 0x0: + result = _ALWAYS + elif timestamp == 0xFFFFFFFFFFFFFFFF: + result = _FOREVER + else: + try: + result = datetime.utcfromtimestamp(timestamp) + except OverflowError as e: + raise ValueError + return result + + @staticmethod + def _time_string_to_datetime(time_string): + result = None + if time_string == 'always': + result = _ALWAYS + elif time_string == 'forever': + result = _FOREVER + elif is_relative_time_string(time_string): + result = convert_relative_to_datetime(time_string) + else: + for time_format in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"): + try: + result = datetime.strptime(time_string, time_format) + except ValueError: + pass + if result is None: + raise ValueError + return result + + +class OpensshCertificateOption(object): + def __init__(self, option_type, name, data): + if option_type not in ('critical', 'extension'): + raise ValueError("type must be either 'critical' or 'extension'") + + if not isinstance(name, six.string_types): + raise TypeError("name must be a string not %s" % type(name)) + + if not isinstance(data, six.string_types): + raise TypeError("data must be a string not %s" % type(data)) + + self._option_type = option_type + self._name = name.lower() + self._data = data + + def __eq__(self, other): + if not isinstance(other, type(self)): + return NotImplemented + + return all([ + self._option_type == other._option_type, + self._name == other._name, + self._data == other._data, + ]) + + def __hash__(self): + return hash((self._option_type, self._name, self._data)) + + def __ne__(self, other): + return not self == other + + def __str__(self): + if self._data: + return "%s=%s" % (self._name, self._data) + return self._name + + @property + def data(self): + return self._data + + @property + def name(self): + return self._name + + @property + def type(self): + return self._option_type + + @classmethod + def from_string(cls, option_string): + if not isinstance(option_string, six.string_types): + raise ValueError("option_string must be a string not %s" % type(option_string)) + option_type = None + + if ':' in option_string: + option_type, value = option_string.strip().split(':', 1) + if '=' in value: + name, data = value.split('=', 1) + else: + name, data = value, '' + elif '=' in option_string: + name, data = option_string.strip().split('=', 1) + else: + name, data = option_string.strip(), '' + + return cls( + option_type=option_type or get_option_type(name.lower()), + name=name, + data=data + ) + + +@six.add_metaclass(abc.ABCMeta) +class OpensshCertificateInfo: + """Encapsulates all certificate information which is signed by a CA key""" + def __init__(self, + nonce=None, + serial=None, + cert_type=None, + key_id=None, + principals=None, + valid_after=None, + valid_before=None, + critical_options=None, + extensions=None, + reserved=None, + signing_key=None): + self.nonce = nonce + self.serial = serial + self._cert_type = cert_type + self.key_id = key_id + self.principals = principals + self.valid_after = valid_after + self.valid_before = valid_before + self.critical_options = critical_options + self.extensions = extensions + self.reserved = reserved + self.signing_key = signing_key + + self.type_string = None + + @property + def cert_type(self): + if self._cert_type == _USER_TYPE: + return 'user' + elif self._cert_type == _HOST_TYPE: + return 'host' + else: + return '' + + @cert_type.setter + def cert_type(self, cert_type): + if cert_type == 'user' or cert_type == _USER_TYPE: + self._cert_type = _USER_TYPE + elif cert_type == 'host' or cert_type == _HOST_TYPE: + self._cert_type = _HOST_TYPE + else: + raise ValueError("%s is not a valid certificate type" % cert_type) + + def signing_key_fingerprint(self): + return fingerprint(self.signing_key) + + @abc.abstractmethod + def public_key_fingerprint(self): + pass + + @abc.abstractmethod + def parse_public_numbers(self, parser): + pass + + +class OpensshRSACertificateInfo(OpensshCertificateInfo): + def __init__(self, e=None, n=None, **kwargs): + super(OpensshRSACertificateInfo, self).__init__(**kwargs) + self.type_string = _SSH_TYPE_STRINGS['rsa'] + _CERT_SUFFIX_V01 + self.e = e + self.n = n + + # See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 + def public_key_fingerprint(self): + if any([self.e is None, self.n is None]): + return b'' + + writer = _OpensshWriter() + writer.string(_SSH_TYPE_STRINGS['rsa']) + writer.mpint(self.e) + writer.mpint(self.n) + + return fingerprint(writer.bytes()) + + def parse_public_numbers(self, parser): + self.e = parser.mpint() + self.n = parser.mpint() + + +class OpensshDSACertificateInfo(OpensshCertificateInfo): + def __init__(self, p=None, q=None, g=None, y=None, **kwargs): + super(OpensshDSACertificateInfo, self).__init__(**kwargs) + self.type_string = _SSH_TYPE_STRINGS['dsa'] + _CERT_SUFFIX_V01 + self.p = p + self.q = q + self.g = g + self.y = y + + # See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 + def public_key_fingerprint(self): + if any([self.p is None, self.q is None, self.g is None, self.y is None]): + return b'' + + writer = _OpensshWriter() + writer.string(_SSH_TYPE_STRINGS['dsa']) + writer.mpint(self.p) + writer.mpint(self.q) + writer.mpint(self.g) + writer.mpint(self.y) + + return fingerprint(writer.bytes()) + + def parse_public_numbers(self, parser): + self.p = parser.mpint() + self.q = parser.mpint() + self.g = parser.mpint() + self.y = parser.mpint() + + +class OpensshECDSACertificateInfo(OpensshCertificateInfo): + def __init__(self, curve=None, public_key=None, **kwargs): + super(OpensshECDSACertificateInfo, self).__init__(**kwargs) + self._curve = None + if curve is not None: + self.curve = curve + + self.public_key = public_key + + @property + def curve(self): + return self._curve + + @curve.setter + def curve(self, curve): + if curve in _ECDSA_CURVE_IDENTIFIERS.values(): + self._curve = curve + self.type_string = _SSH_TYPE_STRINGS[_ECDSA_CURVE_IDENTIFIERS_LOOKUP[curve]] + _CERT_SUFFIX_V01 + else: + raise ValueError( + "Curve must be one of %s" % (b','.join(list(_ECDSA_CURVE_IDENTIFIERS.values()))).decode('UTF-8') + ) + + # See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 + def public_key_fingerprint(self): + if any([self.curve is None, self.public_key is None]): + return b'' + + writer = _OpensshWriter() + writer.string(_SSH_TYPE_STRINGS[_ECDSA_CURVE_IDENTIFIERS_LOOKUP[self.curve]]) + writer.string(self.curve) + writer.string(self.public_key) + + return fingerprint(writer.bytes()) + + def parse_public_numbers(self, parser): + self.curve = parser.string() + self.public_key = parser.string() + + +class OpensshED25519CertificateInfo(OpensshCertificateInfo): + def __init__(self, pk=None, **kwargs): + super(OpensshED25519CertificateInfo, self).__init__(**kwargs) + self.type_string = _SSH_TYPE_STRINGS['ed25519'] + _CERT_SUFFIX_V01 + self.pk = pk + + def public_key_fingerprint(self): + if self.pk is None: + return b'' + + writer = _OpensshWriter() + writer.string(_SSH_TYPE_STRINGS['ed25519']) + writer.string(self.pk) + + return fingerprint(writer.bytes()) + + def parse_public_numbers(self, parser): + self.pk = parser.string() + + +# See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD +class OpensshCertificate(object): + """Encapsulates a formatted OpenSSH certificate including signature and signing key""" + def __init__(self, cert_info, signature): + + self._cert_info = cert_info + self.signature = signature + + @classmethod + def load(cls, path): + if not os.path.exists(path): + raise ValueError("%s is not a valid path." % path) + + try: + with open(path, 'rb') as cert_file: + data = cert_file.read() + except (IOError, OSError) as e: + raise ValueError("%s cannot be opened for reading: %s" % (path, e)) + + try: + format_identifier, b64_cert = data.split(b' ')[:2] + cert = binascii.a2b_base64(b64_cert) + except (binascii.Error, ValueError): + raise ValueError("Certificate not in OpenSSH format") + + for key_type, string in _SSH_TYPE_STRINGS.items(): + if format_identifier == string + _CERT_SUFFIX_V01: + pub_key_type = key_type + break + else: + raise ValueError("Invalid certificate format identifier: %s" % format_identifier) + + parser = OpensshParser(cert) + + if format_identifier != parser.string(): + raise ValueError("Certificate formats do not match") + + try: + cert_info = cls._parse_cert_info(pub_key_type, parser) + signature = parser.string() + except (TypeError, ValueError) as e: + raise ValueError("Invalid certificate data: %s" % e) + + if parser.remaining_bytes(): + raise ValueError( + "%s bytes of additional data was not parsed while loading %s" % (parser.remaining_bytes(), path) + ) + + return cls( + cert_info=cert_info, + signature=signature, + ) + + @property + def type_string(self): + return to_text(self._cert_info.type_string) + + @property + def nonce(self): + return self._cert_info.nonce + + @property + def public_key(self): + return to_text(self._cert_info.public_key_fingerprint()) + + @property + def serial(self): + return self._cert_info.serial + + @property + def type(self): + return self._cert_info.cert_type + + @property + def key_id(self): + return to_text(self._cert_info.key_id) + + @property + def principals(self): + return [to_text(p) for p in self._cert_info.principals] + + @property + def valid_after(self): + return self._cert_info.valid_after + + @property + def valid_before(self): + return self._cert_info.valid_before + + @property + def critical_options(self): + return [ + OpensshCertificateOption('critical', to_text(n), to_text(d)) for n, d in self._cert_info.critical_options + ] + + @property + def extensions(self): + return [OpensshCertificateOption('extension', to_text(n), to_text(d)) for n, d in self._cert_info.extensions] + + @property + def reserved(self): + return self._cert_info.reserved + + @property + def signing_key(self): + return to_text(self._cert_info.signing_key_fingerprint()) + + @property + def signature_type(self): + signature_data = OpensshParser.signature_data(self.signature) + return to_text(signature_data['signature_type']) + + @staticmethod + def _parse_cert_info(pub_key_type, parser): + cert_info = get_cert_info_object(pub_key_type) + cert_info.nonce = parser.string() + cert_info.parse_public_numbers(parser) + cert_info.serial = parser.uint64() + cert_info.cert_type = parser.uint32() + cert_info.key_id = parser.string() + cert_info.principals = parser.string_list() + cert_info.valid_after = parser.uint64() + cert_info.valid_before = parser.uint64() + cert_info.critical_options = parser.option_list() + cert_info.extensions = parser.option_list() + cert_info.reserved = parser.string() + cert_info.signing_key = parser.string() + + return cert_info + + def to_dict(self): + time_parameters = OpensshCertificateTimeParameters( + valid_from=self.valid_after, + valid_to=self.valid_before + ) + return { + 'type_string': self.type_string, + 'nonce': self.nonce, + 'serial': self.serial, + 'cert_type': self.type, + 'identifier': self.key_id, + 'principals': self.principals, + 'valid_after': time_parameters.valid_from(date_format='human_readable'), + 'valid_before': time_parameters.valid_to(date_format='human_readable'), + 'critical_options': [str(critical_option) for critical_option in self.critical_options], + 'extensions': [str(extension) for extension in self.extensions], + 'reserved': self.reserved, + 'public_key': self.public_key, + 'signing_key': self.signing_key, + } + + +def apply_directives(directives): + if any(d not in _DIRECTIVES for d in directives): + raise ValueError("directives must be one of %s" % ", ".join(_DIRECTIVES)) + + directive_to_option = { + 'no-x11-forwarding': OpensshCertificateOption('extension', 'permit-x11-forwarding', ''), + 'no-agent-forwarding': OpensshCertificateOption('extension', 'permit-agent-forwarding', ''), + 'no-port-forwarding': OpensshCertificateOption('extension', 'permit-port-forwarding', ''), + 'no-pty': OpensshCertificateOption('extension', 'permit-pty', ''), + 'no-user-rc': OpensshCertificateOption('extension', 'permit-user-rc', ''), + } + + if 'clear' in directives: + return [] + else: + return list(set(default_options()) - set(directive_to_option[d] for d in directives)) + + +def default_options(): + return [OpensshCertificateOption('extension', name, '') for name in _EXTENSIONS] + + +def fingerprint(public_key): + """Generates a SHA256 hash and formats output to resemble ``ssh-keygen``""" + h = sha256() + h.update(public_key) + return b'SHA256:' + b64encode(h.digest()).rstrip(b'=') + + +def get_cert_info_object(key_type): + if key_type == 'rsa': + cert_info = OpensshRSACertificateInfo() + elif key_type == 'dsa': + cert_info = OpensshDSACertificateInfo() + elif key_type in ('ecdsa-nistp256', 'ecdsa-nistp384', 'ecdsa-nistp521'): + cert_info = OpensshECDSACertificateInfo() + elif key_type == 'ed25519': + cert_info = OpensshED25519CertificateInfo() + else: + raise ValueError("%s is not a valid key type" % key_type) + + return cert_info + + +def get_option_type(name): + if name in _CRITICAL_OPTIONS: + result = 'critical' + elif name in _EXTENSIONS: + result = 'extension' + else: + raise ValueError("%s is not a valid option. " % name + + "Custom options must start with 'critical:' or 'extension:' to indicate type") + return result + + +def is_relative_time_string(time_string): + return time_string.startswith("+") or time_string.startswith("-") + + +def parse_option_list(option_list): + critical_options = [] + directives = [] + extensions = [] + + for option in option_list: + if option.lower() in _DIRECTIVES: + directives.append(option.lower()) + else: + option_object = OpensshCertificateOption.from_string(option) + if option_object.type == 'critical': + critical_options.append(option_object) + else: + extensions.append(option_object) + + return critical_options, list(set(extensions + apply_directives(directives))) diff --git a/ansible_collections/community/crypto/plugins/module_utils/openssh/cryptography.py b/ansible_collections/community/crypto/plugins/module_utils/openssh/cryptography.py new file mode 100644 index 000000000..69f3ce354 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/openssh/cryptography.py @@ -0,0 +1,685 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +from base64 import b64encode, b64decode +from getpass import getuser +from socket import gethostname + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +try: + from cryptography import __version__ as CRYPTOGRAPHY_VERSION + from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm + from cryptography.hazmat.backends.openssl import backend + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa, padding + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey + + if LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion("3.0"): + HAS_OPENSSH_PRIVATE_FORMAT = True + else: + HAS_OPENSSH_PRIVATE_FORMAT = False + + HAS_OPENSSH_SUPPORT = True + + _ALGORITHM_PARAMETERS = { + 'rsa': { + 'default_size': 2048, + 'valid_sizes': range(1024, 16384), + 'signer_params': { + 'padding': padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.MAX_LENGTH, + ), + 'algorithm': hashes.SHA256(), + }, + }, + 'dsa': { + 'default_size': 1024, + 'valid_sizes': [1024], + 'signer_params': { + 'algorithm': hashes.SHA256(), + }, + }, + 'ed25519': { + 'default_size': 256, + 'valid_sizes': [256], + 'signer_params': {}, + }, + 'ecdsa': { + 'default_size': 256, + 'valid_sizes': [256, 384, 521], + 'signer_params': { + 'signature_algorithm': ec.ECDSA(hashes.SHA256()), + }, + 'curves': { + 256: ec.SECP256R1(), + 384: ec.SECP384R1(), + 521: ec.SECP521R1(), + } + } + } +except ImportError: + HAS_OPENSSH_PRIVATE_FORMAT = False + HAS_OPENSSH_SUPPORT = False + CRYPTOGRAPHY_VERSION = "0.0" + _ALGORITHM_PARAMETERS = {} + +_TEXT_ENCODING = 'UTF-8' + + +class OpenSSHError(Exception): + pass + + +class InvalidAlgorithmError(OpenSSHError): + pass + + +class InvalidCommentError(OpenSSHError): + pass + + +class InvalidDataError(OpenSSHError): + pass + + +class InvalidPrivateKeyFileError(OpenSSHError): + pass + + +class InvalidPublicKeyFileError(OpenSSHError): + pass + + +class InvalidKeyFormatError(OpenSSHError): + pass + + +class InvalidKeySizeError(OpenSSHError): + pass + + +class InvalidKeyTypeError(OpenSSHError): + pass + + +class InvalidPassphraseError(OpenSSHError): + pass + + +class InvalidSignatureError(OpenSSHError): + pass + + +class AsymmetricKeypair(object): + """Container for newly generated asymmetric key pairs or those loaded from existing files""" + + @classmethod + def generate(cls, keytype='rsa', size=None, passphrase=None): + """Returns an Asymmetric_Keypair object generated with the supplied parameters + or defaults to an unencrypted RSA-2048 key + + :keytype: One of rsa, dsa, ecdsa, ed25519 + :size: The key length for newly generated keys + :passphrase: Secret of type Bytes used to encrypt the private key being generated + """ + + if keytype not in _ALGORITHM_PARAMETERS.keys(): + raise InvalidKeyTypeError( + "%s is not a valid keytype. Valid keytypes are %s" % ( + keytype, ", ".join(_ALGORITHM_PARAMETERS.keys()) + ) + ) + + if not size: + size = _ALGORITHM_PARAMETERS[keytype]['default_size'] + else: + if size not in _ALGORITHM_PARAMETERS[keytype]['valid_sizes']: + raise InvalidKeySizeError( + "%s is not a valid key size for %s keys" % (size, keytype) + ) + + if passphrase: + encryption_algorithm = get_encryption_algorithm(passphrase) + else: + encryption_algorithm = serialization.NoEncryption() + + if keytype == 'rsa': + privatekey = rsa.generate_private_key( + # Public exponent should always be 65537 to prevent issues + # if improper padding is used during signing + public_exponent=65537, + key_size=size, + backend=backend, + ) + elif keytype == 'dsa': + privatekey = dsa.generate_private_key( + key_size=size, + backend=backend, + ) + elif keytype == 'ed25519': + privatekey = Ed25519PrivateKey.generate() + elif keytype == 'ecdsa': + privatekey = ec.generate_private_key( + _ALGORITHM_PARAMETERS['ecdsa']['curves'][size], + backend=backend, + ) + + publickey = privatekey.public_key() + + return cls( + keytype=keytype, + size=size, + privatekey=privatekey, + publickey=publickey, + encryption_algorithm=encryption_algorithm + ) + + @classmethod + def load(cls, path, passphrase=None, private_key_format='PEM', public_key_format='PEM', no_public_key=False): + """Returns an Asymmetric_Keypair object loaded from the supplied file path + + :path: A path to an existing private key to be loaded + :passphrase: Secret of type bytes used to decrypt the private key being loaded + :private_key_format: Format of private key to be loaded + :public_key_format: Format of public key to be loaded + :no_public_key: Set 'True' to only load a private key and automatically populate the matching public key + """ + + if passphrase: + encryption_algorithm = get_encryption_algorithm(passphrase) + else: + encryption_algorithm = serialization.NoEncryption() + + privatekey = load_privatekey(path, passphrase, private_key_format) + if no_public_key: + publickey = privatekey.public_key() + else: + publickey = load_publickey(path + '.pub', public_key_format) + + # Ed25519 keys are always of size 256 and do not have a key_size attribute + if isinstance(privatekey, Ed25519PrivateKey): + size = _ALGORITHM_PARAMETERS['ed25519']['default_size'] + else: + size = privatekey.key_size + + if isinstance(privatekey, rsa.RSAPrivateKey): + keytype = 'rsa' + elif isinstance(privatekey, dsa.DSAPrivateKey): + keytype = 'dsa' + elif isinstance(privatekey, ec.EllipticCurvePrivateKey): + keytype = 'ecdsa' + elif isinstance(privatekey, Ed25519PrivateKey): + keytype = 'ed25519' + else: + raise InvalidKeyTypeError("Key type '%s' is not supported" % type(privatekey)) + + return cls( + keytype=keytype, + size=size, + privatekey=privatekey, + publickey=publickey, + encryption_algorithm=encryption_algorithm + ) + + def __init__(self, keytype, size, privatekey, publickey, encryption_algorithm): + """ + :keytype: One of rsa, dsa, ecdsa, ed25519 + :size: The key length for the private key of this key pair + :privatekey: Private key object of this key pair + :publickey: Public key object of this key pair + :encryption_algorithm: Hashed secret used to encrypt the private key of this key pair + """ + + self.__size = size + self.__keytype = keytype + self.__privatekey = privatekey + self.__publickey = publickey + self.__encryption_algorithm = encryption_algorithm + + try: + self.verify(self.sign(b'message'), b'message') + except InvalidSignatureError: + raise InvalidPublicKeyFileError( + "The private key and public key of this keypair do not match" + ) + + def __eq__(self, other): + if not isinstance(other, AsymmetricKeypair): + return NotImplemented + + return (compare_publickeys(self.public_key, other.public_key) and + compare_encryption_algorithms(self.encryption_algorithm, other.encryption_algorithm)) + + def __ne__(self, other): + return not self == other + + @property + def private_key(self): + """Returns the private key of this key pair""" + + return self.__privatekey + + @property + def public_key(self): + """Returns the public key of this key pair""" + + return self.__publickey + + @property + def size(self): + """Returns the size of the private key of this key pair""" + + return self.__size + + @property + def key_type(self): + """Returns the key type of this key pair""" + + return self.__keytype + + @property + def encryption_algorithm(self): + """Returns the key encryption algorithm of this key pair""" + + return self.__encryption_algorithm + + def sign(self, data): + """Returns signature of data signed with the private key of this key pair + + :data: byteslike data to sign + """ + + try: + signature = self.__privatekey.sign( + data, + **_ALGORITHM_PARAMETERS[self.__keytype]['signer_params'] + ) + except TypeError as e: + raise InvalidDataError(e) + + return signature + + def verify(self, signature, data): + """Verifies that the signature associated with the provided data was signed + by the private key of this key pair. + + :signature: signature to verify + :data: byteslike data signed by the provided signature + """ + try: + return self.__publickey.verify( + signature, + data, + **_ALGORITHM_PARAMETERS[self.__keytype]['signer_params'] + ) + except InvalidSignature: + raise InvalidSignatureError + + def update_passphrase(self, passphrase=None): + """Updates the encryption algorithm of this key pair + + :passphrase: Byte secret used to encrypt this key pair + """ + + if passphrase: + self.__encryption_algorithm = get_encryption_algorithm(passphrase) + else: + self.__encryption_algorithm = serialization.NoEncryption() + + +class OpensshKeypair(object): + """Container for OpenSSH encoded asymmetric key pairs""" + + @classmethod + def generate(cls, keytype='rsa', size=None, passphrase=None, comment=None): + """Returns an Openssh_Keypair object generated using the supplied parameters or defaults to a RSA-2048 key + + :keytype: One of rsa, dsa, ecdsa, ed25519 + :size: The key length for newly generated keys + :passphrase: Secret of type Bytes used to encrypt the newly generated private key + :comment: Comment for a newly generated OpenSSH public key + """ + + if comment is None: + comment = "%s@%s" % (getuser(), gethostname()) + + asym_keypair = AsymmetricKeypair.generate(keytype, size, passphrase) + openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, 'SSH') + openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment) + fingerprint = calculate_fingerprint(openssh_publickey) + + return cls( + asym_keypair=asym_keypair, + openssh_privatekey=openssh_privatekey, + openssh_publickey=openssh_publickey, + fingerprint=fingerprint, + comment=comment, + ) + + @classmethod + def load(cls, path, passphrase=None, no_public_key=False): + """Returns an Openssh_Keypair object loaded from the supplied file path + + :path: A path to an existing private key to be loaded + :passphrase: Secret used to decrypt the private key being loaded + :no_public_key: Set 'True' to only load a private key and automatically populate the matching public key + """ + + if no_public_key: + comment = "" + else: + comment = extract_comment(path + '.pub') + + asym_keypair = AsymmetricKeypair.load(path, passphrase, 'SSH', 'SSH', no_public_key) + openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, 'SSH') + openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment) + fingerprint = calculate_fingerprint(openssh_publickey) + + return cls( + asym_keypair=asym_keypair, + openssh_privatekey=openssh_privatekey, + openssh_publickey=openssh_publickey, + fingerprint=fingerprint, + comment=comment, + ) + + @staticmethod + def encode_openssh_privatekey(asym_keypair, key_format): + """Returns an OpenSSH encoded private key for a given keypair + + :asym_keypair: Asymmetric_Keypair from the private key is extracted + :key_format: Format of the encoded private key. + """ + + if key_format == 'SSH': + # Default to PEM format if SSH not available + if not HAS_OPENSSH_PRIVATE_FORMAT: + privatekey_format = serialization.PrivateFormat.PKCS8 + else: + privatekey_format = serialization.PrivateFormat.OpenSSH + elif key_format == 'PKCS8': + privatekey_format = serialization.PrivateFormat.PKCS8 + elif key_format == 'PKCS1': + if asym_keypair.key_type == 'ed25519': + raise InvalidKeyFormatError("ed25519 keys cannot be represented in PKCS1 format") + privatekey_format = serialization.PrivateFormat.TraditionalOpenSSL + else: + raise InvalidKeyFormatError("The accepted private key formats are SSH, PKCS8, and PKCS1") + + encoded_privatekey = asym_keypair.private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=privatekey_format, + encryption_algorithm=asym_keypair.encryption_algorithm + ) + + return encoded_privatekey + + @staticmethod + def encode_openssh_publickey(asym_keypair, comment): + """Returns an OpenSSH encoded public key for a given keypair + + :asym_keypair: Asymmetric_Keypair from the public key is extracted + :comment: Comment to apply to the end of the returned OpenSSH encoded public key + """ + encoded_publickey = asym_keypair.public_key.public_bytes( + encoding=serialization.Encoding.OpenSSH, + format=serialization.PublicFormat.OpenSSH, + ) + + validate_comment(comment) + + encoded_publickey += (" %s" % comment).encode(encoding=_TEXT_ENCODING) if comment else b'' + + return encoded_publickey + + def __init__(self, asym_keypair, openssh_privatekey, openssh_publickey, fingerprint, comment): + """ + :asym_keypair: An Asymmetric_Keypair object from which the OpenSSH encoded keypair is derived + :openssh_privatekey: An OpenSSH encoded private key + :openssh_privatekey: An OpenSSH encoded public key + :fingerprint: The fingerprint of the OpenSSH encoded public key of this keypair + :comment: Comment applied to the OpenSSH public key of this keypair + """ + + self.__asym_keypair = asym_keypair + self.__openssh_privatekey = openssh_privatekey + self.__openssh_publickey = openssh_publickey + self.__fingerprint = fingerprint + self.__comment = comment + + def __eq__(self, other): + if not isinstance(other, OpensshKeypair): + return NotImplemented + + return self.asymmetric_keypair == other.asymmetric_keypair and self.comment == other.comment + + @property + def asymmetric_keypair(self): + """Returns the underlying asymmetric key pair of this OpenSSH encoded key pair""" + + return self.__asym_keypair + + @property + def private_key(self): + """Returns the OpenSSH formatted private key of this key pair""" + + return self.__openssh_privatekey + + @property + def public_key(self): + """Returns the OpenSSH formatted public key of this key pair""" + + return self.__openssh_publickey + + @property + def size(self): + """Returns the size of the private key of this key pair""" + + return self.__asym_keypair.size + + @property + def key_type(self): + """Returns the key type of this key pair""" + + return self.__asym_keypair.key_type + + @property + def fingerprint(self): + """Returns the fingerprint (SHA256 Hash) of the public key of this key pair""" + + return self.__fingerprint + + @property + def comment(self): + """Returns the comment applied to the OpenSSH formatted public key of this key pair""" + + return self.__comment + + @comment.setter + def comment(self, comment): + """Updates the comment applied to the OpenSSH formatted public key of this key pair + + :comment: Text to update the OpenSSH public key comment + """ + + validate_comment(comment) + + self.__comment = comment + encoded_comment = (" %s" % self.__comment).encode(encoding=_TEXT_ENCODING) if self.__comment else b'' + self.__openssh_publickey = b' '.join(self.__openssh_publickey.split(b' ', 2)[:2]) + encoded_comment + return self.__openssh_publickey + + def update_passphrase(self, passphrase): + """Updates the passphrase used to encrypt the private key of this keypair + + :passphrase: Text secret used for encryption + """ + + self.__asym_keypair.update_passphrase(passphrase) + self.__openssh_privatekey = OpensshKeypair.encode_openssh_privatekey(self.__asym_keypair, 'SSH') + + +def load_privatekey(path, passphrase, key_format): + privatekey_loaders = { + 'PEM': serialization.load_pem_private_key, + 'DER': serialization.load_der_private_key, + } + + # OpenSSH formatted private keys are not available in Cryptography <3.0 + if hasattr(serialization, 'load_ssh_private_key'): + privatekey_loaders['SSH'] = serialization.load_ssh_private_key + else: + privatekey_loaders['SSH'] = serialization.load_pem_private_key + + try: + privatekey_loader = privatekey_loaders[key_format] + except KeyError: + raise InvalidKeyFormatError( + "%s is not a valid key format (%s)" % ( + key_format, + ','.join(privatekey_loaders.keys()) + ) + ) + + if not os.path.exists(path): + raise InvalidPrivateKeyFileError("No file was found at %s" % path) + + try: + with open(path, 'rb') as f: + content = f.read() + + privatekey = privatekey_loader( + data=content, + password=passphrase, + backend=backend, + ) + + except ValueError as e: + # Revert to PEM if key could not be loaded in SSH format + if key_format == 'SSH': + try: + privatekey = privatekey_loaders['PEM']( + data=content, + password=passphrase, + backend=backend, + ) + except ValueError as e: + raise InvalidPrivateKeyFileError(e) + except TypeError as e: + raise InvalidPassphraseError(e) + except UnsupportedAlgorithm as e: + raise InvalidAlgorithmError(e) + else: + raise InvalidPrivateKeyFileError(e) + except TypeError as e: + raise InvalidPassphraseError(e) + except UnsupportedAlgorithm as e: + raise InvalidAlgorithmError(e) + + return privatekey + + +def load_publickey(path, key_format): + publickey_loaders = { + 'PEM': serialization.load_pem_public_key, + 'DER': serialization.load_der_public_key, + 'SSH': serialization.load_ssh_public_key, + } + + try: + publickey_loader = publickey_loaders[key_format] + except KeyError: + raise InvalidKeyFormatError( + "%s is not a valid key format (%s)" % ( + key_format, + ','.join(publickey_loaders.keys()) + ) + ) + + if not os.path.exists(path): + raise InvalidPublicKeyFileError("No file was found at %s" % path) + + try: + with open(path, 'rb') as f: + content = f.read() + + publickey = publickey_loader( + data=content, + backend=backend, + ) + except ValueError as e: + raise InvalidPublicKeyFileError(e) + except UnsupportedAlgorithm as e: + raise InvalidAlgorithmError(e) + + return publickey + + +def compare_publickeys(pk1, pk2): + a = isinstance(pk1, Ed25519PublicKey) + b = isinstance(pk2, Ed25519PublicKey) + if a or b: + if not a or not b: + return False + a = pk1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + b = pk2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + return a == b + else: + return pk1.public_numbers() == pk2.public_numbers() + + +def compare_encryption_algorithms(ea1, ea2): + if isinstance(ea1, serialization.NoEncryption) and isinstance(ea2, serialization.NoEncryption): + return True + elif (isinstance(ea1, serialization.BestAvailableEncryption) and + isinstance(ea2, serialization.BestAvailableEncryption)): + return ea1.password == ea2.password + else: + return False + + +def get_encryption_algorithm(passphrase): + try: + return serialization.BestAvailableEncryption(passphrase) + except ValueError as e: + raise InvalidPassphraseError(e) + + +def validate_comment(comment): + if not hasattr(comment, 'encode'): + raise InvalidCommentError("%s cannot be encoded to text" % comment) + + +def extract_comment(path): + + if not os.path.exists(path): + raise InvalidPublicKeyFileError("No file was found at %s" % path) + + try: + with open(path, 'rb') as f: + fields = f.read().split(b' ', 2) + if len(fields) == 3: + comment = fields[2].decode(_TEXT_ENCODING) + else: + comment = "" + except (IOError, OSError) as e: + raise InvalidPublicKeyFileError(e) + + return comment + + +def calculate_fingerprint(openssh_publickey): + digest = hashes.Hash(hashes.SHA256(), backend=backend) + decoded_pubkey = b64decode(openssh_publickey.split(b' ')[1]) + digest.update(decoded_pubkey) + + return 'SHA256:%s' % b64encode(digest.finalize()).decode(encoding=_TEXT_ENCODING).rstrip('=') diff --git a/ansible_collections/community/crypto/plugins/module_utils/openssh/utils.py b/ansible_collections/community/crypto/plugins/module_utils/openssh/utils.py new file mode 100644 index 000000000..0c3af8f24 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/openssh/utils.py @@ -0,0 +1,392 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2020, Doug Stanley <doug+ansible@technologixllc.com> +# Copyright (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import re +from contextlib import contextmanager +from struct import Struct + +from ansible.module_utils.six import PY3 + +# Protocol References +# ------------------- +# https://datatracker.ietf.org/doc/html/rfc4251 +# https://datatracker.ietf.org/doc/html/rfc4253 +# https://datatracker.ietf.org/doc/html/rfc5656 +# https://datatracker.ietf.org/doc/html/rfc8032 +# +# Inspired by: +# ------------ +# https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/serialization/ssh.py +# https://github.com/paramiko/paramiko/blob/master/paramiko/message.py + +if PY3: + long = int + +# 0 (False) or 1 (True) encoded as a single byte +_BOOLEAN = Struct(b'?') +# Unsigned 8-bit integer in network-byte-order +_UBYTE = Struct(b'!B') +_UBYTE_MAX = 0xFF +# Unsigned 32-bit integer in network-byte-order +_UINT32 = Struct(b'!I') +# Unsigned 32-bit little endian integer +_UINT32_LE = Struct(b'<I') +_UINT32_MAX = 0xFFFFFFFF +# Unsigned 64-bit integer in network-byte-order +_UINT64 = Struct(b'!Q') +_UINT64_MAX = 0xFFFFFFFFFFFFFFFF + + +def any_in(sequence, *elements): + return any(e in sequence for e in elements) + + +def file_mode(path): + if not os.path.exists(path): + return 0o000 + return os.stat(path).st_mode & 0o777 + + +def parse_openssh_version(version_string): + """Parse the version output of ssh -V and return version numbers that can be compared""" + + parsed_result = re.match( + r"^.*openssh_(?P<version>[0-9.]+)(p?[0-9]+)[^0-9]*.*$", version_string.lower() + ) + if parsed_result is not None: + version = parsed_result.group("version").strip() + else: + version = None + + return version + + +@contextmanager +def secure_open(path, mode): + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode) + try: + yield fd + finally: + os.close(fd) + + +def secure_write(path, mode, content): + with secure_open(path, mode) as fd: + os.write(fd, content) + + +# See https://datatracker.ietf.org/doc/html/rfc4251#section-5 for SSH data types +class OpensshParser(object): + """Parser for OpenSSH encoded objects""" + BOOLEAN_OFFSET = 1 + UINT32_OFFSET = 4 + UINT64_OFFSET = 8 + + def __init__(self, data): + if not isinstance(data, (bytes, bytearray)): + raise TypeError("Data must be bytes-like not %s" % type(data)) + + self._data = memoryview(data) if PY3 else data + self._pos = 0 + + def boolean(self): + next_pos = self._check_position(self.BOOLEAN_OFFSET) + + value = _BOOLEAN.unpack(self._data[self._pos:next_pos])[0] + self._pos = next_pos + return value + + def uint32(self): + next_pos = self._check_position(self.UINT32_OFFSET) + + value = _UINT32.unpack(self._data[self._pos:next_pos])[0] + self._pos = next_pos + return value + + def uint64(self): + next_pos = self._check_position(self.UINT64_OFFSET) + + value = _UINT64.unpack(self._data[self._pos:next_pos])[0] + self._pos = next_pos + return value + + def string(self): + length = self.uint32() + + next_pos = self._check_position(length) + + value = self._data[self._pos:next_pos] + self._pos = next_pos + # Cast to bytes is required as a memoryview slice is itself a memoryview + return value if not PY3 else bytes(value) + + def mpint(self): + return self._big_int(self.string(), "big", signed=True) + + def name_list(self): + raw_string = self.string() + return raw_string.decode('ASCII').split(',') + + # Convenience function, but not an official data type from SSH + def string_list(self): + result = [] + raw_string = self.string() + + if raw_string: + parser = OpensshParser(raw_string) + while parser.remaining_bytes(): + result.append(parser.string()) + + return result + + # Convenience function, but not an official data type from SSH + def option_list(self): + result = [] + raw_string = self.string() + + if raw_string: + parser = OpensshParser(raw_string) + + while parser.remaining_bytes(): + name = parser.string() + data = parser.string() + if data: + # data is doubly-encoded + data = OpensshParser(data).string() + result.append((name, data)) + + return result + + def seek(self, offset): + self._pos = self._check_position(offset) + + return self._pos + + def remaining_bytes(self): + return len(self._data) - self._pos + + def _check_position(self, offset): + if self._pos + offset > len(self._data): + raise ValueError("Insufficient data remaining at position: %s" % self._pos) + elif self._pos + offset < 0: + raise ValueError("Position cannot be less than zero.") + else: + return self._pos + offset + + @classmethod + def signature_data(cls, signature_string): + signature_data = {} + + parser = cls(signature_string) + signature_type = parser.string() + signature_blob = parser.string() + + blob_parser = cls(signature_blob) + if signature_type in (b'ssh-rsa', b'rsa-sha2-256', b'rsa-sha2-512'): + # https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 + # https://datatracker.ietf.org/doc/html/rfc8332#section-3 + signature_data['s'] = cls._big_int(signature_blob, "big") + elif signature_type == b'ssh-dss': + # https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 + signature_data['r'] = cls._big_int(signature_blob[:20], "big") + signature_data['s'] = cls._big_int(signature_blob[20:], "big") + elif signature_type in (b'ecdsa-sha2-nistp256', b'ecdsa-sha2-nistp384', b'ecdsa-sha2-nistp521'): + # https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2 + signature_data['r'] = blob_parser.mpint() + signature_data['s'] = blob_parser.mpint() + elif signature_type == b'ssh-ed25519': + # https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2 + signature_data['R'] = cls._big_int(signature_blob[:32], "little") + signature_data['S'] = cls._big_int(signature_blob[32:], "little") + else: + raise ValueError("%s is not a valid signature type" % signature_type) + + signature_data['signature_type'] = signature_type + + return signature_data + + @classmethod + def _big_int(cls, raw_string, byte_order, signed=False): + if byte_order not in ("big", "little"): + raise ValueError("Byte_order must be one of (big, little) not %s" % byte_order) + + if PY3: + return int.from_bytes(raw_string, byte_order, signed=signed) + + result = 0 + byte_length = len(raw_string) + + if byte_length > 0: + # Check sign-bit + msb = raw_string[0] if byte_order == "big" else raw_string[-1] + negative = bool(ord(msb) & 0x80) + # Match pad value for two's complement + pad = b'\xFF' if signed and negative else b'\x00' + # The definition of ``mpint`` enforces that unnecessary bytes are not encoded so they are added back + pad_length = (4 - byte_length % 4) + if pad_length < 4: + raw_string = pad * pad_length + raw_string if byte_order == "big" else raw_string + pad * pad_length + byte_length += pad_length + # Accumulate arbitrary precision integer bytes in the appropriate order + if byte_order == "big": + for i in range(0, byte_length, cls.UINT32_OFFSET): + left_shift = result << cls.UINT32_OFFSET * 8 + result = left_shift + _UINT32.unpack(raw_string[i:i + cls.UINT32_OFFSET])[0] + else: + for i in range(byte_length, 0, -cls.UINT32_OFFSET): + left_shift = result << cls.UINT32_OFFSET * 8 + result = left_shift + _UINT32_LE.unpack(raw_string[i - cls.UINT32_OFFSET:i])[0] + # Adjust for two's complement + if signed and negative: + result -= 1 << (8 * byte_length) + + return result + + +class _OpensshWriter(object): + """Writes SSH encoded values to a bytes-like buffer + + .. warning:: + This class is a private API and must not be exported outside of the openssh module_utils. + It is not to be used to construct Openssh objects, but rather as a utility to assist + in validating parsed material. + """ + def __init__(self, buffer=None): + if buffer is not None: + if not isinstance(buffer, (bytes, bytearray)): + raise TypeError("Buffer must be a bytes-like object not %s" % type(buffer)) + else: + buffer = bytearray() + + self._buff = buffer + + def boolean(self, value): + if not isinstance(value, bool): + raise TypeError("Value must be of type bool not %s" % type(value)) + + self._buff.extend(_BOOLEAN.pack(value)) + + return self + + def uint32(self, value): + if not isinstance(value, int): + raise TypeError("Value must be of type int not %s" % type(value)) + if value < 0 or value > _UINT32_MAX: + raise ValueError("Value must be a positive integer less than %s" % _UINT32_MAX) + + self._buff.extend(_UINT32.pack(value)) + + return self + + def uint64(self, value): + if not isinstance(value, (long, int)): + raise TypeError("Value must be of type (long, int) not %s" % type(value)) + if value < 0 or value > _UINT64_MAX: + raise ValueError("Value must be a positive integer less than %s" % _UINT64_MAX) + + self._buff.extend(_UINT64.pack(value)) + + return self + + def string(self, value): + if not isinstance(value, (bytes, bytearray)): + raise TypeError("Value must be bytes-like not %s" % type(value)) + self.uint32(len(value)) + self._buff.extend(value) + + return self + + def mpint(self, value): + if not isinstance(value, (int, long)): + raise TypeError("Value must be of type (long, int) not %s" % type(value)) + + self.string(self._int_to_mpint(value)) + + return self + + def name_list(self, value): + if not isinstance(value, list): + raise TypeError("Value must be a list of byte strings not %s" % type(value)) + + try: + self.string(','.join(value).encode('ASCII')) + except UnicodeEncodeError as e: + raise ValueError("Name-list's must consist of US-ASCII characters: %s" % e) + + return self + + def string_list(self, value): + if not isinstance(value, list): + raise TypeError("Value must be a list of byte string not %s" % type(value)) + + writer = _OpensshWriter() + for s in value: + writer.string(s) + + self.string(writer.bytes()) + + return self + + def option_list(self, value): + if not isinstance(value, list) or (value and not isinstance(value[0], tuple)): + raise TypeError("Value must be a list of tuples") + + writer = _OpensshWriter() + for name, data in value: + writer.string(name) + # SSH option data is encoded twice though this behavior is not documented + writer.string(_OpensshWriter().string(data).bytes() if data else bytes()) + + self.string(writer.bytes()) + + return self + + @staticmethod + def _int_to_mpint(num): + if PY3: + byte_length = (num.bit_length() + 7) // 8 + try: + result = num.to_bytes(byte_length, "big", signed=True) + # Handles values which require \x00 or \xFF to pad sign-bit + except OverflowError: + result = num.to_bytes(byte_length + 1, "big", signed=True) + else: + result = bytes() + # 0 and -1 are treated as special cases since they are used as sentinels for all other values + if num == 0: + result += b'\x00' + elif num == -1: + result += b'\xFF' + elif num > 0: + while num >> 32: + result = _UINT32.pack(num & _UINT32_MAX) + result + num = num >> 32 + # Pack last 4 bytes individually to discard insignificant bytes + while num: + result = _UBYTE.pack(num & _UBYTE_MAX) + result + num = num >> 8 + # Zero pad final byte if most-significant bit is 1 as per mpint definition + if ord(result[0]) & 0x80: + result = b'\x00' + result + else: + while (num >> 32) < -1: + result = _UINT32.pack(num & _UINT32_MAX) + result + num = num >> 32 + while num < -1: + result = _UBYTE.pack(num & _UBYTE_MAX) + result + num = num >> 8 + if not ord(result[0]) & 0x80: + result = b'\xFF' + result + + return result + + def bytes(self): + return bytes(self._buff) diff --git a/ansible_collections/community/crypto/plugins/module_utils/version.py b/ansible_collections/community/crypto/plugins/module_utils/version.py new file mode 100644 index 000000000..dc01ffe8f --- /dev/null +++ b/ansible_collections/community/crypto/plugins/module_utils/version.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Provide version object to compare version numbers.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +# Once we drop support for Ansible 2.9, ansible-base 2.10, and ansible-core 2.11, we can +# remove the _version.py file, and replace the following import by +# +# from ansible.module_utils.compat.version import LooseVersion + +from ._version import LooseVersion # noqa: F401, pylint: disable=unused-import diff --git a/ansible_collections/community/crypto/plugins/modules/acme_account.py b/ansible_collections/community/crypto/plugins/modules/acme_account.py new file mode 100644 index 000000000..13de49ab0 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/acme_account.py @@ -0,0 +1,345 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: acme_account +author: "Felix Fontein (@felixfontein)" +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(community.crypto.acme_certificate) module also allows to do basic account management. + When using both modules, it is recommended to disable account management + for M(community.crypto.acme_certificate). For that, use the C(modify_account) option of + M(community.crypto.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: community.crypto.acme_account_info + description: Retrieves facts about an ACME account. + - module: community.crypto.openssl_privatekey + description: Can be used to create a private account key. + - module: community.crypto.openssl_privatekey_pipe + description: Can be used to create a private account key without writing it to disk. + - module: community.crypto.acme_inspect + description: Allows to debug problems. +extends_documentation_fragment: + - community.crypto.acme + - community.crypto.attributes + - community.crypto.attributes.actiongroup_acme +attributes: + check_mode: + support: full + diff_mode: + support: full +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: true + 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: false + 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 + new_account_key_passphrase: + description: + - Phassphrase to use to decode the new account key. + - "B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend." + type: str + version_added: 1.6.0 + external_account_binding: + description: + - Allows to provide external account binding data during account creation. + - This is used by CAs like Sectigo to bind a new ACME account to an existing CA-specific + account, to be able to properly identify a customer. + - Only used when creating a new account. Can not be specified for ACME v1. + type: dict + suboptions: + kid: + description: + - The key identifier provided by the CA. + type: str + required: true + alg: + description: + - The MAC algorithm provided by the CA. + - If not specified by the CA, this is probably C(HS256). + type: str + required: true + choices: [ HS256, HS384, HS512 ] + key: + description: + - Base64 URL encoded value of the MAC key provided by the CA. + - Padding (C(=) symbols at the end) can be omitted. + type: str + required: true + version_added: 1.1.0 +''' + +EXAMPLES = ''' +- name: Make sure account exists and has given contacts. We agree to TOS. + community.crypto.acme_account: + account_key_src: /etc/pki/cert/private/account.key + state: present + terms_agreed: true + contact: + - mailto:me@example.com + - mailto:myself@example.org + +- name: Make sure account has given email address. Do not create account if it does not exist + community.crypto.acme_account: + account_key_src: /etc/pki/cert/private/account.key + state: present + allow_creation: false + contact: + - mailto:me@example.com + +- name: Change account's key to the one stored in the variable new_account_key + community.crypto.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) + community.crypto.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 +''' + +import base64 + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( + create_backend, + get_default_argspec, + ACMEClient, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.account import ( + ACMEAccount, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + ModuleFailException, + KeyParsingError, +) + + +def main(): + argument_spec = get_default_argspec() + argument_spec.update(dict( + terms_agreed=dict(type='bool', default=False), + state=dict(type='str', required=True, choices=['absent', 'present', 'changed_key']), + allow_creation=dict(type='bool', default=True), + contact=dict(type='list', elements='str', default=[]), + new_account_key_src=dict(type='path'), + new_account_key_content=dict(type='str', no_log=True), + new_account_key_passphrase=dict(type='str', no_log=True), + external_account_binding=dict(type='dict', options=dict( + kid=dict(type='str', required=True), + alg=dict(type='str', required=True, choices=['HS256', 'HS384', 'HS512']), + key=dict(type='str', required=True, no_log=True), + )) + )) + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=( + ['account_key_src', 'account_key_content'], + ), + mutually_exclusive=( + ['account_key_src', 'account_key_content'], + ['new_account_key_src', 'new_account_key_content'], + ), + required_if=( + # Make sure that for state == changed_key, one of + # new_account_key_src and new_account_key_content are specified + ['state', 'changed_key', ['new_account_key_src', 'new_account_key_content'], True], + ), + supports_check_mode=True, + ) + backend = create_backend(module, True) + + if module.params['external_account_binding']: + # Make sure padding is there + key = module.params['external_account_binding']['key'] + if len(key) % 4 != 0: + key = key + ('=' * (4 - (len(key) % 4))) + # Make sure key is Base64 encoded + try: + base64.urlsafe_b64decode(key) + except Exception as e: + module.fail_json(msg='Key for external_account_binding must be Base64 URL encoded (%s)' % e) + module.params['external_account_binding']['key'] = key + + try: + client = ACMEClient(module, backend) + account = ACMEAccount(client) + changed = False + state = module.params.get('state') + diff_before = {} + diff_after = {} + if state == 'absent': + created, account_data = account.setup_account(allow_creation=False) + if account_data: + diff_before = dict(account_data) + diff_before['public_account_key'] = client.account_key_data['jwk'] + if created: + raise AssertionError('Unwanted account creation') + if account_data is not None: + # Account is not yet deactivated + if not module.check_mode: + # Deactivate it + payload = { + 'status': 'deactivated' + } + result, info = client.send_signed_request( + client.account_uri, payload, error_msg='Failed to deactivate account', expected_status_codes=[200]) + changed = True + elif state == 'present': + allow_creation = module.params.get('allow_creation') + contact = [str(v) for v in module.params.get('contact')] + terms_agreed = module.params.get('terms_agreed') + external_account_binding = module.params.get('external_account_binding') + created, account_data = account.setup_account( + contact, + terms_agreed=terms_agreed, + allow_creation=allow_creation, + external_account_binding=external_account_binding, + ) + if account_data is None: + raise ModuleFailException(msg='Account does not exist or is deactivated.') + if created: + diff_before = {} + else: + diff_before = dict(account_data) + diff_before['public_account_key'] = client.account_key_data['jwk'] + updated = False + if not created: + updated, account_data = account.update_account(account_data, contact) + changed = created or updated + diff_after = dict(account_data) + diff_after['public_account_key'] = client.account_key_data['jwk'] + elif state == 'changed_key': + # Parse new account key + try: + new_key_data = client.parse_key( + module.params.get('new_account_key_src'), + module.params.get('new_account_key_content'), + passphrase=module.params.get('new_account_key_passphrase'), + ) + except KeyParsingError as e: + raise ModuleFailException("Error while parsing new account key: {msg}".format(msg=e.msg)) + # Verify that the account exists and has not been deactivated + created, account_data = account.setup_account(allow_creation=False) + if created: + raise AssertionError('Unwanted account creation') + if account_data is None: + raise ModuleFailException(msg='Account does not exist or is deactivated.') + diff_before = dict(account_data) + diff_before['public_account_key'] = client.account_key_data['jwk'] + # Now we can start the account key rollover + if not module.check_mode: + # Compose inner signed message + # https://tools.ietf.org/html/rfc8555#section-7.3.5 + url = client.directory['keyChange'] + protected = { + "alg": new_key_data['alg'], + "jwk": new_key_data['jwk'], + "url": url, + } + payload = { + "account": client.account_uri, + "newKey": new_key_data['jwk'], # specified in draft 12 and older + "oldKey": client.account_jwk, # specified in draft 13 and newer + } + data = client.sign_request(protected, payload, new_key_data) + # Send request and verify result + result, info = client.send_signed_request( + url, data, error_msg='Failed to rollover account key', expected_status_codes=[200]) + if module._diff: + client.account_key_data = new_key_data + client.account_jws_header['alg'] = new_key_data['alg'] + diff_after = account.get_account_data() + elif module._diff: + # Kind of fake diff_after + diff_after = dict(diff_before) + diff_after['public_account_key'] = new_key_data['jwk'] + changed = True + result = { + 'changed': changed, + 'account_uri': client.account_uri, + } + if module._diff: + result['diff'] = { + 'before': diff_before, + 'after': diff_after, + } + module.exit_json(**result) + except ModuleFailException as e: + e.do_fail(module) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/acme_account_info.py b/ansible_collections/community/crypto/plugins/modules/acme_account_info.py new file mode 100644 index 000000000..4e1a3c7b7 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/acme_account_info.py @@ -0,0 +1,320 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2018 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: acme_account_info +author: "Felix Fontein (@felixfontein)" +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(community.crypto.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." +extends_documentation_fragment: + - community.crypto.acme + - community.crypto.attributes + - community.crypto.attributes.actiongroup_acme + - community.crypto.attributes.info_module +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." + - "If the value is not C(ignore) and the ACME server supports orders, the C(order_uris) + return value is always populated. The C(orders) return value is only returned + if this option is set to C(object_list)." + - "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 +seealso: + - module: community.crypto.acme_account + description: Allows to create, modify or delete an ACME account. + +''' + +EXAMPLES = ''' +- name: Check whether an account with the given account key exists + community.crypto.acme_account_info: + account_key_src: /etc/pki/cert/private/account.key + register: account_data +- name: Verify that account exists + ansible.builtin.assert: + that: + - account_data.exists +- name: Print account URI + ansible.builtin.debug: + var: account_data.account_uri +- name: Print account contacts + ansible.builtin.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 + ansible.builtin.assert: + that: + - account_data.exists +- name: Print account contacts + ansible.builtin.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." + type: list + elements: dict + returned: if account exists, I(retrieve_orders) is C(object_list), 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 + +order_uris: + 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: str + returned: if account exists, I(retrieve_orders) is not C(ignore), and server supports order listing + version_added: 1.5.0 +''' + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( + create_backend, + get_default_argspec, + ACMEClient, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.account import ( + ACMEAccount, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException + +from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( + process_links, +) + + +def get_orders_list(module, client, orders_url): + ''' + Retrieves orders list (handles pagination). + ''' + orders = [] + while orders_url: + # Get part of orders list + res, info = client.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(client, order_url): + ''' + Retrieve order data. + ''' + return client.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, + ) + backend = create_backend(module, True) + + try: + client = ACMEClient(module, backend) + account = ACMEAccount(client) + # Check whether account exists + created, account_data = account.setup_account( + [], + allow_creation=False, + remove_account_uri_if_not_exists=True, + ) + if created: + raise AssertionError('Unwanted account creation') + result = { + 'changed': False, + 'exists': client.account_uri is not None, + 'account_uri': client.account_uri, + } + if client.account_uri is not None: + # Make sure promised data is there + if 'contact' not in account_data: + account_data['contact'] = [] + account_data['public_account_key'] = client.account_key_data['jwk'] + result['account'] = account_data + # Retrieve orders list + if account_data.get('orders') and module.params['retrieve_orders'] != 'ignore': + orders = get_orders_list(module, client, account_data['orders']) + result['order_uris'] = orders + if module.params['retrieve_orders'] == 'object_list': + result['orders'] = [get_order(client, order) for order in orders] + module.exit_json(**result) + except ModuleFailException as e: + e.do_fail(module) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/acme_certificate.py b/ansible_collections/community/crypto/plugins/modules/acme_certificate.py new file mode 100644 index 000000000..6aec44e08 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/acme_certificate.py @@ -0,0 +1,950 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: acme_certificate +author: "Michael Gruener (@mgruener)" +short_description: Create SSL/TLS certificates with the ACME protocol +description: + - "Create and renew SSL/TLS certificates with a CA supporting the + L(ACME protocol,https://tools.ietf.org/html/rfc8555), + such as L(Let's Encrypt,https://letsencrypt.org/) or + L(Buypass,https://www.buypass.com/). The current implementation + supports the C(http-01), C(dns-01) and C(tls-alpn-01) challenges." + - "To use this module, it has to be executed twice. Either as two + different tasks in the same run or during two runs. Note that the output + of the first run needs to be recorded and passed to the second run as the + module argument C(data)." + - "Between these two tasks you have to fulfill the required steps for the + chosen challenge by whatever means necessary. For C(http-01) that means + creating the necessary challenge file on the destination webserver. For + C(dns-01) the necessary dns record has to be created. For C(tls-alpn-01) + the necessary certificate has to be created and served. + It is I(not) the responsibility of this module to perform these steps." + - "For details on how to fulfill these challenges, you might have to read through + L(the main ACME specification,https://tools.ietf.org/html/rfc8555#section-8) + and the L(TLS-ALPN-01 specification,https://www.rfc-editor.org/rfc/rfc8737.html#section-3). + Also, consider the examples provided for this module." + - "The module includes experimental support for IP identifiers according to + the L(RFC 8738,https://www.rfc-editor.org/rfc/rfc8738.html)." +notes: + - "At least one of C(dest) and C(fullchain_dest) must be specified." + - "This module includes basic account management functionality. + If you want to have more control over your ACME account, use the + M(community.crypto.acme_account) module and disable account management + for this module using the C(modify_account) option." + - "This module was called C(letsencrypt) before Ansible 2.6. The usage + did not change." +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: Buypass Go SSL + description: Documentation for the Buypass Certification Authority. + Provides useful information for example on rate limits. + link: https://www.buypass.com/ssl/products/acme + - 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-05 + - module: community.crypto.acme_challenge_cert_helper + description: Helps preparing C(tls-alpn-01) challenges. + - module: community.crypto.openssl_privatekey + description: Can be used to create private keys (both for certificates and accounts). + - module: community.crypto.openssl_privatekey_pipe + description: Can be used to create private keys without writing it to disk (both for certificates and accounts). + - module: community.crypto.openssl_csr + description: Can be used to create a Certificate Signing Request (CSR). + - module: community.crypto.openssl_csr_pipe + description: Can be used to create a Certificate Signing Request (CSR) without writing it to disk. + - module: community.crypto.certificate_complete_chain + description: Allows to find the root certificate for the returned fullchain. + - module: community.crypto.acme_certificate_revoke + description: Allows to revoke certificates. + - module: community.crypto.acme_account + description: Allows to create, modify or delete an ACME account. + - module: community.crypto.acme_inspect + description: Allows to debug problems. +extends_documentation_fragment: + - community.crypto.acme + - community.crypto.attributes + - community.crypto.attributes.files + - community.crypto.attributes.actiongroup_acme +attributes: + check_mode: + support: full + diff_mode: + support: none + safe_file_operations: + support: full +options: + account_email: + description: + - "The email address associated with this account." + - "It will be used for certificate expiration warnings." + - "Note that when C(modify_account) is not set to C(false) and you also + used the M(community.crypto.acme_account) module to specify more than one contact + for your account, this module will update your account and restrict + it to the (at most one) contact email address specified here." + type: str + agreement: + description: + - "URI to a terms of service document you agree to when using the + ACME v1 service at C(acme_directory)." + - Default is latest gathered from C(acme_directory) URL. + - This option will only be used when C(acme_version) is 1. + type: str + terms_agreed: + description: + - "Boolean indicating whether you agree to the terms of service document." + - "ACME servers can require this to be true." + - This option will only be used when C(acme_version) is not 1. + type: bool + default: false + modify_account: + description: + - "Boolean indicating whether the module should create the account if + necessary, and update its contact data." + - "Set to C(false) if you want to use the M(community.crypto.acme_account) module to manage + your account instead, and to avoid accidental creation of a new account + using an old key if you changed the account key with M(community.crypto.acme_account)." + - "If set to C(false), C(terms_agreed) and C(account_email) are ignored." + type: bool + default: true + challenge: + description: + - The challenge to be performed. + - If set to C(no challenge), no challenge will be used. This is necessary for some private + CAs which use External Account Binding and other means of validating certificate assurance. + For example, an account could be allowed to issue certificates for C(foo.example.com) + without any further validation for a certain period of time. + type: str + default: 'http-01' + choices: + - 'http-01' + - 'dns-01' + - 'tls-alpn-01' + - 'no challenge' + csr: + description: + - "File containing the CSR for the new certificate." + - "Can be created with M(community.crypto.openssl_csr) or C(openssl req ...)." + - "The CSR may contain multiple Subject Alternate Names, but each one + will lead to an individual challenge that must be fulfilled for the + CSR to be signed." + - "I(Note): the private key used to create the CSR I(must not) be the + account key. This is a bad idea from a security point of view, and + the CA should not accept the CSR. The ACME server should return an + error in this case." + - Precisely one of I(csr) or I(csr_content) must be specified. + type: path + aliases: ['src'] + csr_content: + description: + - "Content of the CSR for the new certificate." + - "Can be created with M(community.crypto.openssl_csr_pipe) or C(openssl req ...)." + - "The CSR may contain multiple Subject Alternate Names, but each one + will lead to an individual challenge that must be fulfilled for the + CSR to be signed." + - "I(Note): the private key used to create the CSR I(must not) be the + account key. This is a bad idea from a security point of view, and + the CA should not accept the CSR. The ACME server should return an + error in this case." + - Precisely one of I(csr) or I(csr_content) must be specified. + type: str + version_added: 1.2.0 + data: + description: + - "The data to validate ongoing challenges. This must be specified for + the second run of the module only." + - "The value that must be used here will be provided by a previous use + of this module. See the examples for more details." + - "Note that for ACME v2, only the C(order_uri) entry of C(data) will + be used. For ACME v1, C(data) must be non-empty to indicate the + second stage is active; all needed data will be taken from the + CSR." + - "I(Note): the C(data) option was marked as C(no_log) up to + Ansible 2.5. From Ansible 2.6 on, it is no longer marked this way + as it causes error messages to be come unusable, and C(data) does + not contain any information which can be used without having + access to the account key or which are not public anyway." + type: dict + dest: + description: + - "The destination file for the certificate." + - "Required if C(fullchain_dest) is not specified." + type: path + aliases: ['cert'] + fullchain_dest: + description: + - "The destination file for the full chain (that is, a certificate followed + by chain of intermediate certificates)." + - "Required if C(dest) is not specified." + type: path + aliases: ['fullchain'] + chain_dest: + description: + - If specified, the intermediate certificate will be written to this file. + type: path + aliases: ['chain'] + remaining_days: + description: + - "The number of days the certificate must have left being valid. + If C(cert_days < remaining_days), then it will be renewed. + If the certificate is not renewed, module return values will not + include C(challenge_data)." + - "To make sure that the certificate is renewed in any case, you can + use the C(force) option." + type: int + default: 10 + deactivate_authzs: + description: + - "Deactivate authentication objects (authz) after issuing a certificate, + or when issuing the certificate failed." + - "Authentication objects are bound to an account key and remain valid + for a certain amount of time, and can be used to issue certificates + without having to re-authenticate the domain. This can be a security + concern." + type: bool + default: false + force: + description: + - Enforces the execution of the challenge and validation, even if an + existing certificate is still valid for more than C(remaining_days). + - This is especially helpful when having an updated CSR, for example with + additional domains for which a new certificate is desired. + type: bool + default: false + retrieve_all_alternates: + description: + - "When set to C(true), will retrieve all alternate trust chains offered by the ACME CA. + These will not be written to disk, but will be returned together with the main + chain as C(all_chains). See the documentation for the C(all_chains) return + value for details." + type: bool + default: false + select_chain: + description: + - "Allows to specify criteria by which an (alternate) trust chain can be selected." + - "The list of criteria will be processed one by one until a chain is found + matching a criterium. If such a chain is found, it will be used by the + module instead of the default chain." + - "If a criterium matches multiple chains, the first one matching will be + returned. The order is determined by the ordering of the C(Link) headers + returned by the ACME server and might not be deterministic." + - "Every criterium can consist of multiple different conditions, like I(issuer) + and I(subject). For the criterium to match a chain, all conditions must apply + to the same certificate in the chain." + - "This option can only be used with the C(cryptography) backend." + type: list + elements: dict + version_added: '1.0.0' + suboptions: + test_certificates: + description: + - "Determines which certificates in the chain will be tested." + - "I(all) tests all certificates in the chain (excluding the leaf, which is + identical in all chains)." + - "I(first) only tests the first certificate in the chain, that is the one which + signed the leaf." + - "I(last) only tests the last certificate in the chain, that is the one furthest + away from the leaf. Its issuer is the root certificate of this chain." + type: str + default: all + choices: [first, last, all] + issuer: + description: + - "Allows to specify parts of the issuer of a certificate in the chain must + have to be selected." + - "If I(issuer) is empty, any certificate will match." + - 'An example value would be C({"commonName": "My Preferred CA Root"}).' + type: dict + subject: + description: + - "Allows to specify parts of the subject of a certificate in the chain must + have to be selected." + - "If I(subject) is empty, any certificate will match." + - 'An example value would be C({"CN": "My Preferred CA Intermediate"})' + type: dict + subject_key_identifier: + description: + - "Checks for the SubjectKeyIdentifier extension. This is an identifier based + on the private key of the intermediate certificate." + - "The identifier must be of the form + C(A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1)." + type: str + authority_key_identifier: + description: + - "Checks for the AuthorityKeyIdentifier extension. This is an identifier based + on the private key of the issuer of the intermediate certificate." + - "The identifier must be of the form + C(C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10)." + type: str +''' + +EXAMPLES = r''' +### Example with HTTP challenge ### + +- name: Create a challenge for sample.com using a account key from a variable. + community.crypto.acme_certificate: + account_key_content: "{{ account_private_key }}" + csr: /etc/pki/cert/csr/sample.com.csr + dest: /etc/httpd/ssl/sample.com.crt + register: sample_com_challenge + +# Alternative first step: +- name: Create a challenge for sample.com using a account key from hashi vault. + community.crypto.acme_certificate: + account_key_content: "{{ lookup('hashi_vault', 'secret=secret/account_private_key:value') }}" + csr: /etc/pki/cert/csr/sample.com.csr + fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt + register: sample_com_challenge + +# Alternative first step: +- name: Create a challenge for sample.com using a account key file. + community.crypto.acme_certificate: + account_key_src: /etc/pki/cert/private/account.key + csr_content: "{{ lookup('file', '/etc/pki/cert/csr/sample.com.csr') }}" + dest: /etc/httpd/ssl/sample.com.crt + fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt + register: sample_com_challenge + +# perform the necessary steps to fulfill the challenge +# for example: +# +# - name: Copy http-01 challenge for sample.com +# ansible.builtin.copy: +# dest: /var/www/html/{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource'] }} +# content: "{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}" +# when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge['challenge_data'] +# +# Alternative way: +# +# - name: Copy http-01 challenges +# ansible.builtin.copy: +# dest: /var/www/{{ item.key }}/{{ item.value['http-01']['resource'] }} +# content: "{{ item.value['http-01']['resource_value'] }}" +# loop: "{{ sample_com_challenge.challenge_data | dict2items }}" +# when: sample_com_challenge is changed + +- name: Let the challenge be validated and retrieve the cert and intermediate certificate + community.crypto.acme_certificate: + account_key_src: /etc/pki/cert/private/account.key + csr: /etc/pki/cert/csr/sample.com.csr + dest: /etc/httpd/ssl/sample.com.crt + fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt + chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt + data: "{{ sample_com_challenge }}" + +### Example with DNS challenge against production ACME server ### + +- name: Create a challenge for sample.com using a account key file. + community.crypto.acme_certificate: + account_key_src: /etc/pki/cert/private/account.key + account_email: myself@sample.com + src: /etc/pki/cert/csr/sample.com.csr + cert: /etc/httpd/ssl/sample.com.crt + challenge: dns-01 + acme_directory: https://acme-v01.api.letsencrypt.org/directory + # Renew if the certificate is at least 30 days old + remaining_days: 60 + register: sample_com_challenge + +# perform the necessary steps to fulfill the challenge +# for example: +# +# - name: Create DNS record for sample.com dns-01 challenge +# community.aws.route53: +# zone: sample.com +# record: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].record }}" +# type: TXT +# ttl: 60 +# state: present +# wait: true +# # Note: route53 requires TXT entries to be enclosed in quotes +# value: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].resource_value | regex_replace('^(.*)$', '\"\\1\"') }}" +# when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge.challenge_data +# +# Alternative way: +# +# - name: Create DNS records for dns-01 challenges +# community.aws.route53: +# zone: sample.com +# record: "{{ item.key }}" +# type: TXT +# ttl: 60 +# state: present +# wait: true +# # Note: item.value is a list of TXT entries, and route53 +# # requires every entry to be enclosed in quotes +# value: "{{ item.value | map('regex_replace', '^(.*)$', '\"\\1\"' ) | list }}" +# loop: "{{ sample_com_challenge.challenge_data_dns | dict2items }}" +# when: sample_com_challenge is changed + +- name: Let the challenge be validated and retrieve the cert and intermediate certificate + community.crypto.acme_certificate: + account_key_src: /etc/pki/cert/private/account.key + account_email: myself@sample.com + src: /etc/pki/cert/csr/sample.com.csr + cert: /etc/httpd/ssl/sample.com.crt + fullchain: /etc/httpd/ssl/sample.com-fullchain.crt + chain: /etc/httpd/ssl/sample.com-intermediate.crt + challenge: dns-01 + acme_directory: https://acme-v01.api.letsencrypt.org/directory + remaining_days: 60 + data: "{{ sample_com_challenge }}" + when: sample_com_challenge is changed + +# Alternative second step: +- name: Let the challenge be validated and retrieve the cert and intermediate certificate + community.crypto.acme_certificate: + account_key_src: /etc/pki/cert/private/account.key + account_email: myself@sample.com + src: /etc/pki/cert/csr/sample.com.csr + cert: /etc/httpd/ssl/sample.com.crt + fullchain: /etc/httpd/ssl/sample.com-fullchain.crt + chain: /etc/httpd/ssl/sample.com-intermediate.crt + challenge: tls-alpn-01 + remaining_days: 60 + data: "{{ sample_com_challenge }}" + # We use Let's Encrypt's ACME v2 endpoint + acme_directory: https://acme-v02.api.letsencrypt.org/directory + acme_version: 2 + # The following makes sure that if a chain with /CN=DST Root CA X3 in its issuer is provided + # as an alternative, it will be selected. These are the roots cross-signed by IdenTrust. + # As long as Let's Encrypt provides alternate chains with the cross-signed root(s) when + # switching to their own ISRG Root X1 root, this will use the chain ending with a cross-signed + # root. This chain is more compatible with older TLS clients. + select_chain: + - test_certificates: last + issuer: + CN: DST Root CA X3 + O: Digital Signature Trust Co. + when: sample_com_challenge is changed +''' + +RETURN = ''' +cert_days: + description: The number of days the certificate remains valid. + returned: success + type: int +challenge_data: + description: + - Per identifier / challenge type challenge data. + - Since Ansible 2.8.5, only challenges which are not yet valid are returned. + returned: changed + type: list + elements: dict + contains: + resource: + description: The challenge resource that must be created for validation. + returned: changed + type: str + sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA + resource_original: + description: + - The original challenge resource including type identifier for C(tls-alpn-01) + challenges. + returned: changed and challenge is C(tls-alpn-01) + type: str + sample: DNS:example.com + resource_value: + description: + - The value the resource has to produce for the validation. + - For C(http-01) and C(dns-01) challenges, the value can be used as-is. + - "For C(tls-alpn-01) challenges, note that this return value contains a + Base64 encoded version of the correct binary blob which has to be put + into the acmeValidation x509 extension; see + U(https://www.rfc-editor.org/rfc/rfc8737.html#section-3) + for details. To do this, you might need the C(b64decode) Jinja filter + to extract the binary blob from this return value." + returned: changed + type: str + sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA + record: + description: The full DNS record's name for the challenge. + returned: changed and challenge is C(dns-01) + type: str + sample: _acme-challenge.example.com +challenge_data_dns: + description: + - List of TXT values per DNS record, in case challenge is C(dns-01). + - Since Ansible 2.8.5, only challenges which are not yet valid are returned. + returned: changed + type: dict +authorizations: + description: + - ACME authorization data. + - Maps an identifier to ACME authorization objects. See U(https://tools.ietf.org/html/rfc8555#section-7.1.4). + returned: changed + type: dict + sample: + example.com: + identifier: + type: dns + value: example.com + status: valid + expires: '2022-08-04T01:02:03.45Z' + challenges: + - url: https://example.org/acme/challenge/12345 + type: http-01 + status: valid + token: A5b1C3d2E9f8G7h6 + validated: '2022-08-01T01:01:02.34Z' + wildcard: false +order_uri: + description: ACME order URI. + returned: changed + type: str +finalization_uri: + description: ACME finalization URI. + returned: changed + type: str +account_uri: + description: ACME account URI. + returned: changed + type: str +all_chains: + description: + - When I(retrieve_all_alternates) is set to C(true), the module will query the ACME server + for alternate chains. This return value will contain a list of all chains returned, + the first entry being the main chain returned by the server. + - See L(Section 7.4.2 of RFC8555,https://tools.ietf.org/html/rfc8555#section-7.4.2) for details. + returned: when certificate was retrieved and I(retrieve_all_alternates) is set to C(true) + type: list + elements: dict + contains: + cert: + description: + - The leaf certificate itself, in PEM format. + type: str + returned: always + chain: + description: + - The certificate chain, excluding the root, as concatenated PEM certificates. + type: str + returned: always + full_chain: + description: + - The certificate chain, excluding the root, but including the leaf certificate, + as concatenated PEM certificates. + type: str + returned: always +''' + +import os + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( + create_backend, + get_default_argspec, + ACMEClient, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.account import ( + ACMEAccount, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import ( + combine_identifier, + split_identifier, + wait_for_validation, + Authorization, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import ( + retrieve_acme_v1_certificate, + CertificateChain, + Criterium, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + ModuleFailException, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.io import ( + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.orders import ( + Order, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( + pem_to_der, +) + + +NO_CHALLENGE = 'no challenge' + + +class ACMECertificateClient(object): + ''' + ACME client class. Uses an ACME account object and a CSR to + start and validate ACME challenges and download the respective + certificates. + ''' + + def __init__(self, module, backend): + self.module = module + self.version = module.params['acme_version'] + self.challenge = module.params['challenge'] + # We use None instead of a magic string for 'no challenge' + if self.challenge == NO_CHALLENGE: + self.challenge = None + self.csr = module.params['csr'] + self.csr_content = module.params['csr_content'] + self.dest = module.params.get('dest') + self.fullchain_dest = module.params.get('fullchain_dest') + self.chain_dest = module.params.get('chain_dest') + self.client = ACMEClient(module, backend) + self.account = ACMEAccount(self.client) + self.directory = self.client.directory + self.data = module.params['data'] + self.authorizations = None + self.cert_days = -1 + self.order = None + self.order_uri = self.data.get('order_uri') if self.data else None + self.all_chains = None + self.select_chain_matcher = [] + + if self.module.params['select_chain']: + for criterium_idx, criterium in enumerate(self.module.params['select_chain']): + try: + self.select_chain_matcher.append( + self.client.backend.create_chain_matcher( + Criterium(criterium, index=criterium_idx))) + except ValueError as exc: + self.module.warn('Error while parsing criterium: {error}. Ignoring criterium.'.format(error=exc)) + + # Make sure account exists + modify_account = module.params['modify_account'] + if modify_account or self.version > 1: + contact = [] + if module.params['account_email']: + contact.append('mailto:' + module.params['account_email']) + created, account_data = self.account.setup_account( + contact, + agreement=module.params.get('agreement'), + terms_agreed=module.params.get('terms_agreed'), + allow_creation=modify_account, + ) + if account_data is None: + raise ModuleFailException(msg='Account does not exist or is deactivated.') + updated = False + if not created and account_data and modify_account: + updated, account_data = self.account.update_account(account_data, contact) + self.changed = created or updated + else: + # This happens if modify_account is False and the ACME v1 + # protocol is used. In this case, we do not call setup_account() + # to avoid accidental creation of an account. This is OK + # since for ACME v1, the account URI is not needed to send a + # signed ACME request. + pass + + if self.csr is not None and not os.path.exists(self.csr): + raise ModuleFailException("CSR %s not found" % (self.csr)) + + # Extract list of identifiers from CSR + self.identifiers = self.client.backend.get_csr_identifiers(csr_filename=self.csr, csr_content=self.csr_content) + + def is_first_step(self): + ''' + Return True if this is the first execution of this module, i.e. if a + sufficient data object from a first run has not been provided. + ''' + if self.data is None: + return True + if self.version == 1: + # As soon as self.data is a non-empty object, we are in the second stage. + return not self.data + else: + # We are in the second stage if data.order_uri is given (which has been + # stored in self.order_uri by the constructor). + return self.order_uri is None + + def start_challenges(self): + ''' + Create new authorizations for all identifiers of the CSR, + respectively start a new order for ACME v2. + ''' + self.authorizations = {} + if self.version == 1: + for identifier_type, identifier in self.identifiers: + if identifier_type != 'dns': + raise ModuleFailException('ACME v1 only supports DNS identifiers!') + for identifier_type, identifier in self.identifiers: + authz = Authorization.create(self.client, identifier_type, identifier) + self.authorizations[authz.combined_identifier] = authz + else: + self.order = Order.create(self.client, self.identifiers) + self.order_uri = self.order.url + self.order.load_authorizations(self.client) + self.authorizations.update(self.order.authorizations) + self.changed = True + + def get_challenges_data(self, first_step): + ''' + Get challenge details for the chosen challenge type. + Return a tuple of generic challenge details, and specialized DNS challenge details. + ''' + # Get general challenge data + data = {} + for type_identifier, authz in self.authorizations.items(): + identifier_type, identifier = split_identifier(type_identifier) + # Skip valid authentications: their challenges are already valid + # and do not need to be returned + if authz.status == 'valid': + continue + # We drop the type from the key to preserve backwards compatibility + data[identifier] = authz.get_challenge_data(self.client) + if first_step and self.challenge is not None and self.challenge not in data[identifier]: + raise ModuleFailException("Found no challenge of type '{0}' for identifier {1}!".format( + self.challenge, type_identifier)) + # Get DNS challenge data + data_dns = {} + if self.challenge == 'dns-01': + for identifier, challenges in data.items(): + if self.challenge in challenges: + values = data_dns.get(challenges[self.challenge]['record']) + if values is None: + values = [] + data_dns[challenges[self.challenge]['record']] = values + values.append(challenges[self.challenge]['resource_value']) + return data, data_dns + + def finish_challenges(self): + ''' + Verify challenges for all identifiers of the CSR. + ''' + self.authorizations = {} + + # Step 1: obtain challenge information + if self.version == 1: + # For ACME v1, we attempt to create new authzs. Existing ones + # will be returned instead. + for identifier_type, identifier in self.identifiers: + authz = Authorization.create(self.client, identifier_type, identifier) + self.authorizations[combine_identifier(identifier_type, identifier)] = authz + else: + # For ACME v2, we obtain the order object by fetching the + # order URI, and extract the information from there. + self.order = Order.from_url(self.client, self.order_uri) + self.order.load_authorizations(self.client) + self.authorizations.update(self.order.authorizations) + + # Step 2: validate pending challenges + authzs_to_wait_for = [] + for type_identifier, authz in self.authorizations.items(): + if authz.status == 'pending': + if self.challenge is not None: + authz.call_validate(self.client, self.challenge, wait=False) + authzs_to_wait_for.append(authz) + # If there is no challenge, we must check whether the authz is valid + elif authz.status != 'valid': + authz.raise_error( + 'Status is not "valid", even though no challenge should be necessary', + module=self.client.module, + ) + self.changed = True + + # Step 3: wait for authzs to validate + wait_for_validation(authzs_to_wait_for, self.client) + + def download_alternate_chains(self, cert): + alternate_chains = [] + for alternate in cert.alternates: + try: + alt_cert = CertificateChain.download(self.client, alternate) + except ModuleFailException as e: + self.module.warn('Error while downloading alternative certificate {0}: {1}'.format(alternate, e)) + continue + alternate_chains.append(alt_cert) + return alternate_chains + + def find_matching_chain(self, chains): + for criterium_idx, matcher in enumerate(self.select_chain_matcher): + for chain in chains: + if matcher.match(chain): + self.module.debug('Found matching chain for criterium {0}'.format(criterium_idx)) + return chain + return None + + def get_certificate(self): + ''' + Request a new certificate and write it to the destination file. + First verifies whether all authorizations are valid; if not, aborts + with an error. + ''' + for identifier_type, identifier in self.identifiers: + authz = self.authorizations.get(combine_identifier(identifier_type, identifier)) + if authz is None: + raise ModuleFailException('Found no authorization information for "{identifier}"!'.format( + identifier=combine_identifier(identifier_type, identifier))) + if authz.status != 'valid': + authz.raise_error('Status is "{status}" and not "valid"'.format(status=authz.status), module=self.module) + + if self.version == 1: + cert = retrieve_acme_v1_certificate(self.client, pem_to_der(self.csr, self.csr_content)) + else: + self.order.finalize(self.client, pem_to_der(self.csr, self.csr_content)) + cert = CertificateChain.download(self.client, self.order.certificate_uri) + if self.module.params['retrieve_all_alternates'] or self.select_chain_matcher: + # Retrieve alternate chains + alternate_chains = self.download_alternate_chains(cert) + + # Prepare return value for all alternate chains + if self.module.params['retrieve_all_alternates']: + self.all_chains = [cert.to_json()] + for alt_chain in alternate_chains: + self.all_chains.append(alt_chain.to_json()) + + # Try to select alternate chain depending on criteria + if self.select_chain_matcher: + matching_chain = self.find_matching_chain([cert] + alternate_chains) + if matching_chain: + cert = matching_chain + else: + self.module.debug('Found no matching alternative chain') + + if cert.cert is not None: + pem_cert = cert.cert + chain = cert.chain + + if self.dest and write_file(self.module, self.dest, pem_cert.encode('utf8')): + self.cert_days = self.client.backend.get_cert_days(self.dest) + self.changed = True + + if self.fullchain_dest and write_file(self.module, self.fullchain_dest, (pem_cert + "\n".join(chain)).encode('utf8')): + self.cert_days = self.client.backend.get_cert_days(self.fullchain_dest) + self.changed = True + + if self.chain_dest and write_file(self.module, self.chain_dest, ("\n".join(chain)).encode('utf8')): + self.changed = True + + def deactivate_authzs(self): + ''' + Deactivates all valid authz's. Does not raise exceptions. + https://community.letsencrypt.org/t/authorization-deactivation/19860/2 + https://tools.ietf.org/html/rfc8555#section-7.5.2 + ''' + for authz in self.authorizations.values(): + try: + authz.deactivate(self.client) + except Exception: + # ignore errors + pass + if authz.status != 'deactivated': + self.module.warn(warning='Could not deactivate authz object {0}.'.format(authz.url)) + + +def main(): + argument_spec = get_default_argspec() + argument_spec.update(dict( + modify_account=dict(type='bool', default=True), + account_email=dict(type='str'), + agreement=dict(type='str'), + terms_agreed=dict(type='bool', default=False), + challenge=dict(type='str', default='http-01', choices=['http-01', 'dns-01', 'tls-alpn-01', NO_CHALLENGE]), + csr=dict(type='path', aliases=['src']), + csr_content=dict(type='str'), + data=dict(type='dict'), + dest=dict(type='path', aliases=['cert']), + fullchain_dest=dict(type='path', aliases=['fullchain']), + chain_dest=dict(type='path', aliases=['chain']), + remaining_days=dict(type='int', default=10), + deactivate_authzs=dict(type='bool', default=False), + force=dict(type='bool', default=False), + retrieve_all_alternates=dict(type='bool', default=False), + select_chain=dict(type='list', elements='dict', options=dict( + test_certificates=dict(type='str', default='all', choices=['first', 'last', 'all']), + issuer=dict(type='dict'), + subject=dict(type='dict'), + subject_key_identifier=dict(type='str'), + authority_key_identifier=dict(type='str'), + )), + )) + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=( + ['account_key_src', 'account_key_content'], + ['dest', 'fullchain_dest'], + ['csr', 'csr_content'], + ), + mutually_exclusive=( + ['account_key_src', 'account_key_content'], + ['csr', 'csr_content'], + ), + supports_check_mode=True, + ) + backend = create_backend(module, False) + + try: + if module.params.get('dest'): + cert_days = backend.get_cert_days(module.params['dest']) + else: + cert_days = backend.get_cert_days(module.params['fullchain_dest']) + + if module.params['force'] or cert_days < module.params['remaining_days']: + # If checkmode is active, base the changed state solely on the status + # of the certificate file as all other actions (accessing an account, checking + # the authorization status...) would lead to potential changes of the current + # state + if module.check_mode: + module.exit_json(changed=True, authorizations={}, challenge_data={}, cert_days=cert_days) + else: + client = ACMECertificateClient(module, backend) + client.cert_days = cert_days + other = dict() + is_first_step = client.is_first_step() + if is_first_step: + # First run: start challenges / start new order + client.start_challenges() + else: + # Second run: finish challenges, and get certificate + try: + client.finish_challenges() + client.get_certificate() + if client.all_chains is not None: + other['all_chains'] = client.all_chains + finally: + if module.params['deactivate_authzs']: + client.deactivate_authzs() + data, data_dns = client.get_challenges_data(first_step=is_first_step) + auths = dict() + for k, v in client.authorizations.items(): + # Remove "type:" from key + auths[split_identifier(k)[1]] = v.to_json() + module.exit_json( + changed=client.changed, + authorizations=auths, + finalize_uri=client.order.finalize_uri if client.order else None, + order_uri=client.order_uri, + account_uri=client.client.account_uri, + challenge_data=data, + challenge_data_dns=data_dns, + cert_days=client.cert_days, + **other + ) + else: + module.exit_json(changed=False, cert_days=cert_days) + except ModuleFailException as e: + e.do_fail(module) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/acme_certificate_revoke.py b/ansible_collections/community/crypto/plugins/modules/acme_certificate_revoke.py new file mode 100644 index 000000000..f1922384a --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/acme_certificate_revoke.py @@ -0,0 +1,245 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: acme_certificate_revoke +author: "Felix Fontein (@felixfontein)" +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: community.crypto.acme_inspect + description: Allows to debug problems. +extends_documentation_fragment: + - community.crypto.acme + - community.crypto.attributes + - community.crypto.attributes.actiongroup_acme +attributes: + check_mode: + support: none + diff_mode: + support: none +options: + certificate: + description: + - "Path to the certificate to revoke." + type: path + required: true + 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." + 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." + 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 + private_key_passphrase: + description: + - Phassphrase to use to decode the certificate's private key. + - "B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend." + type: str + version_added: 1.6.0 + 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 + community.crypto.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 + community.crypto.acme_certificate_revoke: + private_key_src: /etc/httpd/ssl/sample.com.key + certificate: /etc/httpd/ssl/sample.com.crt +''' + +RETURN = '''#''' + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( + create_backend, + get_default_argspec, + ACMEClient, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.account import ( + ACMEAccount, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + ACMEProtocolException, + ModuleFailException, + KeyParsingError, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( + nopad_b64, + pem_to_der, +) + + +def main(): + argument_spec = get_default_argspec() + argument_spec.update(dict( + private_key_src=dict(type='path'), + private_key_content=dict(type='str', no_log=True), + private_key_passphrase=dict(type='str', no_log=True), + certificate=dict(type='path', required=True), + revoke_reason=dict(type='int'), + )) + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=( + ['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'], + ), + mutually_exclusive=( + ['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'], + ), + supports_check_mode=False, + ) + backend = create_backend(module, False) + + try: + client = ACMEClient(module, backend) + account = ACMEAccount(client) + # Load certificate + certificate = pem_to_der(module.params.get('certificate')) + certificate = nopad_b64(certificate) + # Construct payload + payload = { + 'certificate': certificate + } + if module.params.get('revoke_reason') is not None: + payload['reason'] = module.params.get('revoke_reason') + # Determine endpoint + if module.params.get('acme_version') == 1: + endpoint = client.directory['revoke-cert'] + payload['resource'] = 'revoke-cert' + else: + endpoint = client.directory['revokeCert'] + # Get hold of private key (if available) and make sure it comes from disk + private_key = module.params.get('private_key_src') + private_key_content = module.params.get('private_key_content') + # Revoke certificate + if private_key or private_key_content: + passphrase = module.params['private_key_passphrase'] + # Step 1: load and parse private key + try: + private_key_data = client.parse_key(private_key, private_key_content, passphrase=passphrase) + except KeyParsingError as e: + raise ModuleFailException("Error while parsing private key: {msg}".format(msg=e.msg)) + # Step 2: sign revokation request with private key + jws_header = { + "alg": private_key_data['alg'], + "jwk": private_key_data['jwk'], + } + result, info = client.send_signed_request( + endpoint, payload, key_data=private_key_data, jws_header=jws_header, fail_on_error=False) + else: + # Step 1: get hold of account URI + created, account_data = account.setup_account(allow_creation=False) + if created: + raise AssertionError('Unwanted account creation') + if account_data is None: + raise ModuleFailException(msg='Account does not exist or is deactivated.') + # Step 2: sign revokation request with account key + result, info = client.send_signed_request(endpoint, payload, fail_on_error=False) + if info['status'] != 200: + already_revoked = False + # Standardized error from draft 14 on (https://tools.ietf.org/html/rfc8555#section-7.6) + if result.get('type') == 'urn:ietf:params:acme:error:alreadyRevoked': + already_revoked = True + else: + # Hack for Boulder errors + if module.params.get('acme_version') == 1: + error_type = 'urn:acme:error:malformed' + else: + error_type = 'urn:ietf:params:acme:error:malformed' + if result.get('type') == error_type and result.get('detail') == 'Certificate already revoked': + # Fallback: boulder returns this in case the certificate was already revoked. + already_revoked = True + # If we know the certificate was already revoked, we do not fail, + # but successfully terminate while indicating no change + if already_revoked: + module.exit_json(changed=False) + raise ACMEProtocolException(module, 'Failed to revoke certificate', info=info, content_json=result) + module.exit_json(changed=True) + except ModuleFailException as e: + e.do_fail(module) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/acme_challenge_cert_helper.py b/ansible_collections/community/crypto/plugins/modules/acme_challenge_cert_helper.py new file mode 100644 index 000000000..1b963e8cc --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/acme_challenge_cert_helper.py @@ -0,0 +1,319 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2018 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: acme_challenge_cert_helper +author: "Felix Fontein (@felixfontein)" +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(community.crypto.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" +extends_documentation_fragment: + - community.crypto.attributes +attributes: + check_mode: + support: none + details: + - This action does not modify state. + diff_mode: + support: N/A + details: + - This action does not modify state. +options: + challenge: + description: + - "The challenge type." + type: str + required: true + choices: + - tls-alpn-01 + challenge_data: + description: + - "The C(challenge_data) entry provided by M(community.crypto.acme_certificate) for the + challenge." + type: dict + required: true + 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 + private_key_passphrase: + description: + - Phassphrase to use to decode the private key. + type: str + version_added: 1.6.0 +''' + +EXAMPLES = ''' +- name: Create challenges for a given CRT for sample.com + community.crypto.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 + community.crypto.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 + community.crypto.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 +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 +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 +''' + +import base64 +import datetime +import sys +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.text.converters import to_bytes, to_text + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException + +from ansible_collections.community.crypto.plugins.module_utils.acme.io import ( + read_file, +) + +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 + 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), + private_key_passphrase=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: + # Some callbacks die when exception is provided with value None + if CRYPTOGRAPHY_IMP_ERR: + module.fail_json(msg=missing_required_lib('cryptography >= 1.3'), exception=CRYPTOGRAPHY_IMP_ERR) + module.fail_json(msg=missing_required_lib('cryptography >= 1.3')) + + 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') + private_key_passphrase = module.params.get('private_key_passphrase') + 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=to_bytes(private_key_passphrase) if private_key_passphrase is not None else 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() diff --git a/ansible_collections/community/crypto/plugins/modules/acme_inspect.py b/ansible_collections/community/crypto/plugins/modules/acme_inspect.py new file mode 100644 index 000000000..d5c96b722 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/acme_inspect.py @@ -0,0 +1,325 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2018 Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: acme_inspect +author: "Felix Fontein (@felixfontein)" +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(community.crypto.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(community.crypto.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: + - community.crypto.acme + - community.crypto.attributes + - community.crypto.attributes.actiongroup_acme +attributes: + check_mode: + support: none + diff_mode: + support: none +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: true +''' + +EXAMPLES = r''' +- name: Get directory + community.crypto.acme_inspect: + acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory + acme_version: 2 + method: directory-only + register: directory + +- name: Create an account + community.crypto.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 + community.crypto.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 + community.crypto.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 + community.crypto.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 + community.crypto.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 + community.crypto.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 + community.crypto.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 + community.crypto.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.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text + +from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( + create_backend, + get_default_argspec, + ACMEClient, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + ACMEProtocolException, + ModuleFailException, +) + + +def main(): + argument_spec = get_default_argspec() + argument_spec.update(dict( + url=dict(type='str'), + method=dict(type='str', choices=['get', 'post', 'directory-only'], default='get'), + content=dict(type='str'), + fail_on_acme_error=dict(type='bool', default=True), + )) + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=( + ['account_key_src', 'account_key_content'], + ), + required_if=( + ['method', 'get', ['url']], + ['method', 'post', ['url', 'content']], + ['method', 'get', ['account_key_src', 'account_key_content'], True], + ['method', 'post', ['account_key_src', 'account_key_content'], True], + ), + ) + backend = create_backend(module, False) + + result = dict() + changed = False + try: + # Get hold of ACMEClient and ACMEAccount objects (includes directory) + client = ACMEClient(module, backend) + method = module.params['method'] + result['directory'] = client.directory.directory + # Do we have to do more requests? + if method != 'directory-only': + url = module.params['url'] + fail_on_acme_error = module.params['fail_on_acme_error'] + # Do request + if method == 'get': + data, info = client.get_request(url, parse_json_result=False, fail_on_error=False) + elif method == 'post': + changed = True # only POSTs can change + data, info = client.send_signed_request( + url, to_bytes(module.params['content']), parse_json_result=False, encode_payload=False, fail_on_error=False) + # Update results + result.update(dict( + headers=info, + output_text=to_native(data), + )) + # See if we can parse the result as JSON + try: + result['output_json'] = module.from_json(to_text(data)) + except Exception as dummy: + pass + # Fail if error was returned + if fail_on_acme_error and info['status'] >= 400: + raise ACMEProtocolException(module, info=info, content=data) + # Done! + module.exit_json(changed=changed, **result) + except ModuleFailException as e: + e.do_fail(module, **result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/certificate_complete_chain.py b/ansible_collections/community/crypto/plugins/modules/certificate_complete_chain.py new file mode 100644 index 000000000..b1862d2ce --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/certificate_complete_chain.py @@ -0,0 +1,375 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: certificate_complete_chain +author: "Felix Fontein (@felixfontein)" +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(community.crypto.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" +extends_documentation_fragment: + - community.crypto.attributes +attributes: + check_mode: + support: full + details: + - This action does not modify state. + diff_mode: + support: N/A + details: + - This action does not modify state. +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: true + 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: true + 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 + community.crypto.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 + ansible.builtin.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 + community.crypto.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 + ansible.builtin.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 + ansible.builtin.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.common.text.converters import to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + split_pem_list, +) + +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.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 + 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 cryptography.exceptions.UnsupportedAlgorithm as dummy: + module.warn('Unsupported algorithm "{0}"'.format(cert.cert.signature_hash_algorithm)) + 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 = [] + for cert_pem in split_pem_list(text): + # 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.certificates_by_issuer = dict() + self.certificate_by_cert = 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) + if cert.cert.subject not in self.certificates_by_issuer: + self.certificates_by_issuer[cert.cert.subject] = [] + self.certificates_by_issuer[cert.cert.subject].append(cert) + self.certificate_by_cert[cert.cert] = 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_parents = self.certificates_by_issuer.get(cert.cert.issuer, []) + for potential_parent in potential_parents: + 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 check_cycle(module, occured_certificates, next): + ''' + Make sure that next is not in occured_certificates so far, and add it. + ''' + next_cert = next.cert + if next_cert in occured_certificates: + module.fail_json(msg='Found cycle while building certificate chain') + occured_certificates.add(next_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 = [] + occured_certificates = set([cert.cert for cert in chain]) + if current.cert in roots.certificate_by_cert: + # Do not try to complete the chain when it's already ending with a root certificate + current = None + while current: + root = roots.find_parent(current) + if root: + check_cycle(module, occured_certificates, root) + completed.append(root) + break + intermediate = intermediates.find_parent(current) + if intermediate: + check_cycle(module, occured_certificates, 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() diff --git a/ansible_collections/community/crypto/plugins/modules/crypto_info.py b/ansible_collections/community/crypto/plugins/modules/crypto_info.py new file mode 100644 index 000000000..1988eb32d --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/crypto_info.py @@ -0,0 +1,337 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: crypto_info +author: "Felix Fontein (@felixfontein)" +short_description: Retrieve cryptographic capabilities +version_added: 2.1.0 +description: + - Retrieve information on cryptographic capabilities. + - The current version retrieves information on the L(Python cryptography library, https://cryptography.io/) available to + Ansible modules, and on the OpenSSL binary C(openssl) found in the path. +extends_documentation_fragment: + - community.crypto.attributes + - community.crypto.attributes.info_module +options: {} +''' + +EXAMPLES = r''' +- name: Retrieve information + community.crypto.crypto_info: + account_key_src: /etc/pki/cert/private/account.key + register: crypto_information + +- name: Show retrieved information + ansible.builtin.debug: + var: crypto_information +''' + +RETURN = r''' +python_cryptography_installed: + description: Whether the L(Python cryptography library, https://cryptography.io/) is installed. + returned: always + type: bool + sample: true + +python_cryptography_import_error: + description: Import error when trying to import the L(Python cryptography library, https://cryptography.io/). + returned: when I(python_cryptography_installed=false) + type: str + +python_cryptography_capabilities: + description: Information on the installed L(Python cryptography library, https://cryptography.io/). + returned: when I(python_cryptography_installed=true) + type: dict + contains: + version: + description: The library version. + type: str + curves: + description: + - List of all supported elliptic curves. + - Theoretically this should be non-empty for version 0.5 and higher, depending on the libssl version used. + type: list + elements: str + has_ec: + description: + - Whether elliptic curves are supported. + - Theoretically this should be the case for version 0.5 and higher, depending on the libssl version used. + type: bool + has_ec_sign: + description: + - Whether signing with elliptic curves is supported. + - Theoretically this should be the case for version 1.5 and higher, depending on the libssl version used. + type: bool + has_ed25519: + description: + - Whether Ed25519 keys are supported. + - Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used. + type: bool + has_ed25519_sign: + description: + - Whether signing with Ed25519 keys is supported. + - Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used. + type: bool + has_ed448: + description: + - Whether Ed448 keys are supported. + - Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used. + type: bool + has_ed448_sign: + description: + - Whether signing with Ed448 keys is supported. + - Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used. + type: bool + has_dsa: + description: + - Whether DSA keys are supported. + - Theoretically this should be the case for version 0.5 and higher. + type: bool + has_dsa_sign: + description: + - Whether signing with DSA keys is supported. + - Theoretically this should be the case for version 1.5 and higher. + type: bool + has_rsa: + description: + - Whether RSA keys are supported. + - Theoretically this should be the case for version 0.5 and higher. + type: bool + has_rsa_sign: + description: + - Whether signing with RSA keys is supported. + - Theoretically this should be the case for version 1.4 and higher. + type: bool + has_x25519: + description: + - Whether X25519 keys are supported. + - Theoretically this should be the case for version 2.0 and higher, depending on the libssl version used. + type: bool + has_x25519_serialization: + description: + - Whether serialization of X25519 keys is supported. + - Theoretically this should be the case for version 2.5 and higher, depending on the libssl version used. + type: bool + has_x448: + description: + - Whether X448 keys are supported. + - Theoretically this should be the case for version 2.5 and higher, depending on the libssl version used. + type: bool + +openssl_present: + description: Whether the OpenSSL binary C(openssl) is installed and can be found in the PATH. + returned: always + type: bool + sample: true + +openssl: + description: Information on the installed OpenSSL binary. + returned: when I(openssl_present=true) + type: dict + contains: + path: + description: Path of the OpenSSL binary. + type: str + sample: /usr/bin/openssl + version: + description: The OpenSSL version. + type: str + sample: 1.1.1m + version_output: + description: The complete output of C(openssl version). + type: str + sample: 'OpenSSL 1.1.1m 14 Dec 2021\n' +''' + +import traceback + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + CRYPTOGRAPHY_HAS_EC, + CRYPTOGRAPHY_HAS_EC_SIGN, + CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED25519_SIGN, + CRYPTOGRAPHY_HAS_ED448, + CRYPTOGRAPHY_HAS_ED448_SIGN, + CRYPTOGRAPHY_HAS_DSA, + CRYPTOGRAPHY_HAS_DSA_SIGN, + CRYPTOGRAPHY_HAS_RSA, + CRYPTOGRAPHY_HAS_RSA_SIGN, + CRYPTOGRAPHY_HAS_X25519, + CRYPTOGRAPHY_HAS_X25519_FULL, + CRYPTOGRAPHY_HAS_X448, + HAS_CRYPTOGRAPHY, +) + +try: + import cryptography + from cryptography.exceptions import UnsupportedAlgorithm +except ImportError: + UnsupportedAlgorithm = Exception + CRYPTOGRAPHY_VERSION = None + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() +else: + CRYPTOGRAPHY_VERSION = cryptography.__version__ + CRYPTOGRAPHY_IMP_ERR = None + + +CURVES = ( + ('secp224r1', 'SECP224R1'), + ('secp256k1', 'SECP256K1'), + ('secp256r1', 'SECP256R1'), + ('secp384r1', 'SECP384R1'), + ('secp521r1', 'SECP521R1'), + ('secp192r1', 'SECP192R1'), + ('sect163k1', 'SECT163K1'), + ('sect163r2', 'SECT163R2'), + ('sect233k1', 'SECT233K1'), + ('sect233r1', 'SECT233R1'), + ('sect283k1', 'SECT283K1'), + ('sect283r1', 'SECT283R1'), + ('sect409k1', 'SECT409K1'), + ('sect409r1', 'SECT409R1'), + ('sect571k1', 'SECT571K1'), + ('sect571r1', 'SECT571R1'), + ('brainpoolP256r1', 'BrainpoolP256R1'), + ('brainpoolP384r1', 'BrainpoolP384R1'), + ('brainpoolP512r1', 'BrainpoolP512R1'), +) + + +def add_crypto_information(module): + result = {} + result['python_cryptography_installed'] = HAS_CRYPTOGRAPHY + if not HAS_CRYPTOGRAPHY: + result['python_cryptography_import_error'] = CRYPTOGRAPHY_IMP_ERR + return result + + has_ed25519 = CRYPTOGRAPHY_HAS_ED25519 + if has_ed25519: + try: + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + Ed25519PrivateKey.from_private_bytes(b'') + except ValueError: + pass + except UnsupportedAlgorithm: + has_ed25519 = False + + has_ed448 = CRYPTOGRAPHY_HAS_ED448 + if has_ed448: + try: + from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey + Ed448PrivateKey.from_private_bytes(b'') + except ValueError: + pass + except UnsupportedAlgorithm: + has_ed448 = False + + has_x25519 = CRYPTOGRAPHY_HAS_X25519 + if has_x25519: + try: + from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey + if CRYPTOGRAPHY_HAS_X25519_FULL: + X25519PrivateKey.from_private_bytes(b'') + else: + # Some versions do not support serialization and deserialization - use generate() instead + X25519PrivateKey.generate() + except ValueError: + pass + except UnsupportedAlgorithm: + has_x25519 = False + + has_x448 = CRYPTOGRAPHY_HAS_X448 + if has_x448: + try: + from cryptography.hazmat.primitives.asymmetric.x448 import X448PrivateKey + X448PrivateKey.from_private_bytes(b'') + except ValueError: + pass + except UnsupportedAlgorithm: + has_x448 = False + + curves = [] + if CRYPTOGRAPHY_HAS_EC: + import cryptography.hazmat.backends + import cryptography.hazmat.primitives.asymmetric.ec + + backend = cryptography.hazmat.backends.default_backend() + for curve_name, constructor_name in CURVES: + ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(constructor_name) + if ecclass: + try: + cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(curve=ecclass(), backend=backend) + curves.append(curve_name) + except UnsupportedAlgorithm: + pass + + info = { + 'version': CRYPTOGRAPHY_VERSION, + 'curves': curves, + 'has_ec': CRYPTOGRAPHY_HAS_EC, + 'has_ec_sign': CRYPTOGRAPHY_HAS_EC_SIGN, + 'has_ed25519': has_ed25519, + 'has_ed25519_sign': has_ed25519 and CRYPTOGRAPHY_HAS_ED25519_SIGN, + 'has_ed448': has_ed448, + 'has_ed448_sign': has_ed448 and CRYPTOGRAPHY_HAS_ED448_SIGN, + 'has_dsa': CRYPTOGRAPHY_HAS_DSA, + 'has_dsa_sign': CRYPTOGRAPHY_HAS_DSA_SIGN, + 'has_rsa': CRYPTOGRAPHY_HAS_RSA, + 'has_rsa_sign': CRYPTOGRAPHY_HAS_RSA_SIGN, + 'has_x25519': has_x25519, + 'has_x25519_serialization': has_x25519 and CRYPTOGRAPHY_HAS_X25519_FULL, + 'has_x448': has_x448, + } + result['python_cryptography_capabilities'] = info + return result + + +def add_openssl_information(module): + openssl_binary = module.get_bin_path('openssl') + result = { + 'openssl_present': openssl_binary is not None, + } + if openssl_binary is None: + return result + + openssl_result = { + 'path': openssl_binary, + } + result['openssl'] = openssl_result + + rc, out, err = module.run_command([openssl_binary, 'version']) + if rc == 0: + openssl_result['version_output'] = out + parts = out.split(None, 2) + if len(parts) > 1: + openssl_result['version'] = parts[1] + + return result + + +INFO_FUNCTIONS = ( + add_crypto_information, + add_openssl_information, +) + + +def main(): + module = AnsibleModule(argument_spec={}, supports_check_mode=True) + result = {} + for fn in INFO_FUNCTIONS: + result.update(fn(module)) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/ecs_certificate.py b/ansible_collections/community/crypto/plugins/modules/ecs_certificate.py new file mode 100644 index 000000000..b19b86f56 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/ecs_certificate.py @@ -0,0 +1,966 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c), Entrust Datacard Corporation, 2019 +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: ecs_certificate +author: + - Chris Trufan (@ctrufan) +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 +extends_documentation_fragment: + - community.crypto.attributes + - community.crypto.attributes.files + - community.crypto.ecs_credential +attributes: + check_mode: + support: partial + details: + - Check mode is only supported if I(request_type=new). + diff_mode: + support: none + safe_file_operations: + support: full +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. + - For exmaple, if you are requesting Certificates with a 90 day lifetime, do not set I(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 C(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 its 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: community.crypto.openssl_privatekey + description: Can be used to create private keys (both for certificates and accounts). + - module: community.crypto.openssl_csr + description: Can be used to create a Certificate Signing Request (CSR). +''' + +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. + community.crypto.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. + community.crypto.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. + community.crypto.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. + community.crypto.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. + community.crypto.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 + community.crypto.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_collections.community.crypto.plugins.module_utils.ecs.api import ( + ecs_client_argument_spec, + ECSClient, + RestOperationException, + SessionConfigurationException, +) + +import datetime +import os +import re +import time +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.text.converters import to_native, to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.io import ( + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + load_certificate, +) + +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 = 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 do not 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) + 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' + 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: + 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' + 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() diff --git a/ansible_collections/community/crypto/plugins/modules/ecs_domain.py b/ansible_collections/community/crypto/plugins/modules/ecs_domain.py new file mode 100644 index 000000000..ec7ad98b0 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/ecs_domain.py @@ -0,0 +1,412 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2019 Entrust Datacard Corporation. +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: ecs_domain +author: + - Chris Trufan (@ctrufan) +version_added: '1.0.0' +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. +extends_documentation_fragment: + - community.crypto.attributes + - community.crypto.ecs_credential +attributes: + check_mode: + support: none + diff_mode: + support: none +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 its 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: community.crypto.x509_certificate + description: Can be used to request certificates from ECS, with C(provider=entrust). + - module: community.crypto.ecs_certificate + description: Can be used to request a Certificate from ECS using a verified domain. +''' + +EXAMPLES = r''' +- name: Request domain validation using email validation for client ID of 2. + community.crypto.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 + community.crypto.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. + community.crypto.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. + community.crypto.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 + +''' + +import datetime +import time + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.ecs.api import ( + ecs_client_argument_spec, + ECSClient, + RestOperationException, + SessionConfigurationException, +) + + +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() diff --git a/ansible_collections/community/crypto/plugins/modules/get_certificate.py b/ansible_collections/community/crypto/plugins/modules/get_certificate.py new file mode 100644 index 000000000..4b2eeaed8 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/get_certificate.py @@ -0,0 +1,416 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: get_certificate +author: "John Westcott IV (@john-westcott-iv)" +short_description: Get a certificate from a host:port +description: + - Makes a secure connection and returns information about the presented certificate + - The module uses the cryptography Python library. + - Support SNI (L(Server Name Indication,https://en.wikipedia.org/wiki/Server_Name_Indication)) only with python >= 2.7. +extends_documentation_fragment: + - community.crypto.attributes +attributes: + check_mode: + support: none + details: + - This action does not modify state. + diff_mode: + support: N/A + details: + - This action does not modify state. +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 + server_name: + description: + - Server name used for SNI (L(Server Name Indication,https://en.wikipedia.org/wiki/Server_Name_Indication)) when hostname + is an IP or is different from server name. + type: str + version_added: 1.4.0 + proxy_host: + description: + - Proxy host used when get a certificate. + type: str + proxy_port: + description: + - Proxy port used when get a certificate. + type: int + default: 8080 + starttls: + description: + - Requests a secure connection for protocols which require clients to initiate encryption. + - Only available for C(mysql) currently. + type: str + choices: + - mysql + version_added: 1.9.0 + 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. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography ] + ciphers: + description: + - SSL/TLS Ciphers to use for the request. + - 'When a list is provided, all ciphers are joined in order with C(:).' + - See the L(OpenSSL Cipher List Format,https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html#CIPHER-LIST-FORMAT) + for more details. + - The available ciphers is dependent on the Python and OpenSSL/LibreSSL versions. + type: list + elements: str + version_added: 2.11.0 + asn1_base64: + description: + - Whether to encode the ASN.1 values in the C(extensions) return value with Base64 or not. + - The documentation claimed for a long time that the values are Base64 encoded, but they + never were. For compatibility this option is set to C(false), but that value will eventually + be deprecated and changed to C(true). + type: bool + default: false + version_added: 2.12.0 + +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" +''' + +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 ASN.1 content of the extension. + - If I(asn1_base64=true) this will be Base64 encoded, otherwise the raw + binary value will be returned. + - Please note that the raw binary value might not survive JSON serialization + to the Ansible controller, and also might cause failures when displaying it. + See U(https://github.com/ansible/ansible/issues/80258) for more information. + - B(Note) that depending on the C(cryptography) version used, it is + not possible to extract the ASN.1 content of the extension, but only + to provide the re-encoded content of the extension in case it was + parsed by C(cryptography). This should usually result in exactly the + same value, except if the original extension value was malformed. + 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 + community.crypto.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 + community.crypto.get_certificate: + host: "www.google.com" + port: 443 + delegate_to: localhost + run_once: true + register: cert + +- name: How many days until cert expires + ansible.builtin.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 }}" +''' + +import atexit +import base64 +import datetime +import traceback + +from os.path import isfile +from socket import create_connection, setdefaulttimeout, socket +from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_REQUIRED + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.text.converters import to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_oid_to_name, + cryptography_get_extensions_from_cert, +) + +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 + +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 send_starttls_packet(sock, server_type): + if server_type == 'mysql': + ssl_request_packet = ( + b'\x20\x00\x00\x01\x85\xae\x7f\x00' + + b'\x00\x00\x00\x01\x21\x00\x00\x00' + + b'\x00\x00\x00\x00\x00\x00\x00\x00' + + b'\x00\x00\x00\x00\x00\x00\x00\x00' + + b'\x00\x00\x00\x00' + ) + + sock.recv(8192) # discard initial handshake from server for this naive implementation + sock.send(ssl_request_packet) + + +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), + server_name=dict(type='str'), + timeout=dict(type='int', default=10), + select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'), + starttls=dict(type='str', choices=['mysql']), + ciphers=dict(type='list', elements='str'), + asn1_base64=dict(type='bool', default=False), + ), + ) + + 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') + server_name = module.params.get('server_name') + start_tls_server_type = module.params.get('starttls') + ciphers = module.params.get('ciphers') + asn1_base64 = module.params['asn1_base64'] + + 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) + + # Try cryptography + if can_use_cryptography: + backend = 'cryptography' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Cannot detect the required Python library " + "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) + + if 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 not HAS_CREATE_DEFAULT_CONTEXT: + # Python < 2.7.9 + if proxy_host: + 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) + if ciphers is not None: + module.fail_json(msg='To use ciphers, you must run the get_certificate module with Python 2.7 or newer.', + exception=CREATE_DEFAULT_CONTEXT_IMP_ERR) + try: + # Note: get_server_certificate does not support SNI! + cert = get_server_certificate((host, port), ca_certs=ca_cert) + except Exception as e: + module.fail_json(msg="Failed to get cert from {0}:{1}, error: {2}".format(host, port, e)) + else: + # Python >= 2.7.9 + try: + if proxy_host: + 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) + else: + sock = create_connection((host, port)) + atexit.register(sock.close) + + if ca_cert: + ctx = create_default_context(cafile=ca_cert) + ctx.check_hostname = False + ctx.verify_mode = CERT_REQUIRED + else: + ctx = create_default_context() + ctx.check_hostname = False + ctx.verify_mode = CERT_NONE + + if start_tls_server_type is not None: + send_starttls_packet(sock, start_tls_server_type) + + if ciphers is not None: + ciphers_joined = ":".join(ciphers) + ctx.set_ciphers(ciphers_joined) + + cert = ctx.wrap_socket(sock, server_hostname=server_name or host).getpeercert(True) + cert = DER_cert_to_PEM_cert(cert) + except Exception as e: + if proxy_host: + module.fail_json(msg="Failed to get cert via proxy {0}:{1} from {2}:{3}, error: {4}".format( + proxy_host, proxy_port, host, port, e)) + else: + module.fail_json(msg="Failed to get cert from {0}:{1}, error: {2}".format(host, port, e)) + + result['cert'] = cert + + if backend == 'cryptography': + x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography_backend()) + result['subject'] = {} + for attribute in x509.subject: + result['subject'][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 cryptography_get_extensions_from_cert(x509).items(): + oid = cryptography.x509.oid.ObjectIdentifier(dotted_number) + ext = { + 'critical': entry['critical'], + 'asn1_data': entry['value'], + 'name': cryptography_oid_to_name(oid, short=True), + } + if not asn1_base64: + ext['asn1_data'] = base64.b64decode(ext['asn1_data']) + result['extensions'].append(ext) + + result['issuer'] = {} + for attribute in x509.issuer: + result['issuer'][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'] = 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() diff --git a/ansible_collections/community/crypto/plugins/modules/luks_device.py b/ansible_collections/community/crypto/plugins/modules/luks_device.py new file mode 100644 index 000000000..d8b70e748 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/luks_device.py @@ -0,0 +1,1031 @@ +#!/usr/bin/python +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: luks_device + +short_description: Manage encrypted (LUKS) devices + +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." + +extends_documentation_fragment: + - community.crypto.attributes + +attributes: + check_mode: + support: full + diff_mode: + support: none + +options: + device: + description: + - "Device to work with (for example 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 + (that is, 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: '1.0.0' + keysize: + description: + - "Sets the key size only if LUKS container does not exist." + type: int + version_added: '1.0.0' + 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 idempotent only since + community.crypto 1.4.0. For older versions, 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 idempotent only since + community.crypto 1.4.0. For older versions, a new keyslot will + be used even if another keyslot already exists for this passphrase." + type: str + version_added: '1.0.0' + 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 idempotent only since + community.crypto 1.4.0. For older versions, 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(true)." + - "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 idempotent only since + community.crypto 1.4.0. For older versions, 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(true)." + type: str + version_added: '1.0.0' + force_remove_last_key: + description: + - "If set to C(true), 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: false + 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: '1.0.0' + 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: '1.0.0' + 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: '1.0.0' + cipher: + description: + - "This option allows the user to define the cipher specification + string for the LUKS container." + - "Will only be used on container creation." + - "For pre-2.6.10 kernels, use C(aes-plain) as they do not understand + the new cipher spec strings. To use ESSIV, use C(aes-cbc-essiv:sha256)." + type: str + version_added: '1.1.0' + hash: + description: + - "This option allows the user to specify the hash function used in LUKS + key setup scheme and volume key digest." + - "Will only be used on container creation." + type: str + version_added: '1.1.0' + pbkdf: + description: + - This option allows the user to configure the Password-Based Key Derivation + Function (PBKDF) used. + - Will only be used on container creation, and when adding keys to an existing + container. + type: dict + version_added: '1.4.0' + suboptions: + iteration_time: + description: + - Specify the iteration time used for the PBKDF. + - Note that this is in B(seconds), not in milliseconds as on the + command line. + - Mutually exclusive with I(iteration_count). + type: float + iteration_count: + description: + - Specify the iteration count used for the PBKDF. + - Mutually exclusive with I(iteration_time). + type: int + algorithm: + description: + - The algorithm to use. + - Only available for the LUKS 2 format. + choices: + - argon2i + - argon2id + - pbkdf2 + type: str + memory: + description: + - The memory cost limit in kilobytes for the PBKDF. + - This is not used for PBKDF2, but only for the Argon PBKDFs. + type: int + parallel: + description: + - The parallel cost for the PBKDF. This is the number of threads that + run in parallel. + - This is not used for PBKDF2, but only for the Argon PBKDFs. + type: int + sector_size: + description: + - "This option allows the user to specify the sector size (in bytes) used for LUKS2 containers." + - "Will only be used on container creation." + type: int + version_added: '1.5.0' + perf_same_cpu_crypt: + description: + - "Allows the user to perform encryption using the same CPU that IO was submitted on." + - "The default is to use an unbound workqueue so that encryption work is automatically balanced between available CPUs." + - "Will only be used when opening containers." + type: bool + default: false + version_added: '2.3.0' + perf_submit_from_crypt_cpus: + description: + - "Allows the user to disable offloading writes to a separate thread after encryption." + - "There are some situations where offloading block write IO operations from the encryption threads + to a single thread degrades performance significantly." + - "The default is to offload block write IO operations to the same thread." + - "Will only be used when opening containers." + type: bool + default: false + version_added: '2.3.0' + perf_no_read_workqueue: + description: + - "Allows the user to bypass dm-crypt internal workqueue and process read requests synchronously." + - "Will only be used when opening containers." + type: bool + default: false + version_added: '2.3.0' + perf_no_write_workqueue: + description: + - "Allows the user to bypass dm-crypt internal workqueue and process write requests synchronously." + - "Will only be used when opening containers." + type: bool + default: false + version_added: '2.3.0' + persistent: + description: + - "Allows the user to store options into container's metadata persistently and automatically use them next time. + Only I(perf_same_cpu_crypt), I(perf_submit_from_crypt_cpus), I(perf_no_read_workqueue), and I(perf_no_write_workqueue) + can be stored persistently." + - "Will only work with LUKS2 containers." + - "Will only be used when opening containers." + type: bool + default: false + version_added: '2.3.0' + +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) + community.crypto.luks_device: + device: "/dev/loop0" + state: "present" + keyfile: "/vault/keyfile" + +- name: Create LUKS container with a passphrase + community.crypto.luks_device: + device: "/dev/loop0" + state: "present" + passphrase: "foo" + +- name: Create LUKS container with specific encryption + community.crypto.luks_device: + device: "/dev/loop0" + state: "present" + cipher: "aes" + hash: "sha256" + +- name: (Create and) open the LUKS container; name it "mycrypt" + community.crypto.luks_device: + device: "/dev/loop0" + state: "opened" + name: "mycrypt" + keyfile: "/vault/keyfile" + +- name: Close the existing LUKS container "mycrypt" + community.crypto.luks_device: + state: "closed" + name: "mycrypt" + +- name: Make sure LUKS container exists and is closed + community.crypto.luks_device: + device: "/dev/loop0" + state: "closed" + keyfile: "/vault/keyfile" + +- name: Create container if it does not exist and add new key to it + community.crypto.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) + community.crypto.luks_device: + device: "/dev/loop0" + keyfile: "/vault/keyfile" + new_keyfile: "/vault/keyfile2" + +- name: Add new passphrase to the LUKS container + community.crypto.luks_device: + device: "/dev/loop0" + keyfile: "/vault/keyfile" + new_passphrase: "foo" + +- name: Remove existing keyfile from the LUKS container + community.crypto.luks_device: + device: "/dev/loop0" + remove_keyfile: "/vault/keyfile2" + +- name: Remove existing passphrase from the LUKS container + community.crypto.luks_device: + device: "/dev/loop0" + remove_passphrase: "foo" + +- name: Completely remove the LUKS container and its contents + community.crypto.luks_device: + device: "/dev/loop0" + state: "absent" + +- name: Create a container with label + community.crypto.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" + community.crypto.luks_device: + label: "personalLabelName" + state: "opened" + name: "mycrypt" + keyfile: "/vault/keyfile" + +- name: Close container based on UUID + community.crypto.luks_device: + uuid: 03ecd578-fad4-4e6c-9348-842e3e8fa340 + state: "closed" + name: "mycrypt" + +- name: Create a container using luks2 format + community.crypto.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'^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*') + + +# See https://gitlab.com/cryptsetup/cryptsetup/-/wikis/LUKS-standard/on-disk-format.pdf +LUKS_HEADER = b'LUKS\xba\xbe' +LUKS_HEADER_L = 6 +# See https://gitlab.com/cryptsetup/LUKS2-docs/-/blob/master/luks2_doc_wip.pdf +LUKS2_HEADER_OFFSETS = [0x4000, 0x8000, 0x10000, 0x20000, 0x40000, 0x80000, 0x100000, 0x200000, 0x400000] +LUKS2_HEADER2 = b'SKUL\xba\xbe' + + +def wipe_luks_headers(device): + wipe_offsets = [] + with open(device, 'rb') as f: + # f.seek(0) + data = f.read(LUKS_HEADER_L) + if data == LUKS_HEADER: + wipe_offsets.append(0) + for offset in LUKS2_HEADER_OFFSETS: + f.seek(offset) + data = f.read(LUKS_HEADER_L) + if data == LUKS2_HEADER2: + wipe_offsets.append(offset) + + if wipe_offsets: + with open(device, 'wb') as f: + for offset in wipe_offsets: + f.seek(offset) + f.write(b'\x00\x00\x00\x00\x00\x00') + + +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])) + + for line in result[STDOUT].splitlines(False): + m = LUKS_NAME_REGEX.match(line) + if m: + return m.group(1) + return None + + 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 _add_pbkdf_options(self, options, pbkdf): + if pbkdf['iteration_time'] is not None: + options.extend(['--iter-time', str(int(pbkdf['iteration_time'] * 1000))]) + if pbkdf['iteration_count'] is not None: + options.extend(['--pbkdf-force-iterations', str(pbkdf['iteration_count'])]) + if pbkdf['algorithm'] is not None: + options.extend(['--pbkdf', pbkdf['algorithm']]) + if pbkdf['memory'] is not None: + options.extend(['--pbkdf-memory', str(pbkdf['memory'])]) + if pbkdf['parallel'] is not None: + options.extend(['--pbkdf-parallel', str(pbkdf['parallel'])]) + + def run_luks_create(self, device, keyfile, passphrase, keysize, cipher, hash_, sector_size, pbkdf): + # 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]) + if cipher is not None: + options.extend(['--cipher', cipher]) + if hash_ is not None: + options.extend(['--hash', hash_]) + if pbkdf is not None: + self._add_pbkdf_options(options, pbkdf) + if sector_size is not None: + options.extend(['--sector-size', str(sector_size)]) + + 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, perf_same_cpu_crypt, perf_submit_from_crypt_cpus, + perf_no_read_workqueue, perf_no_write_workqueue, persistent, name): + args = [self._cryptsetup_bin] + if keyfile: + args.extend(['--key-file', keyfile]) + if perf_same_cpu_crypt: + args.extend(['--perf-same_cpu_crypt']) + if perf_submit_from_crypt_cpus: + args.extend(['--perf-submit_from_crypt_cpus']) + if perf_no_read_workqueue: + args.extend(['--perf-no_read_workqueue']) + if perf_no_write_workqueue: + args.extend(['--perf-no_write_workqueue']) + if persistent: + args.extend(['--persistent']) + 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 signatures for %s: %s' + % (device, result[STDERR])) + + # For LUKS2, sometimes both `cryptsetup erase` and `wipefs` do **not** + # erase all LUKS signatures (they seem to miss the second header). That's + # why we do it ourselves here. + try: + wipe_luks_headers(device) + except Exception as exc: + raise ValueError('Error while wiping LUKS container signatures for %s: %s' % (device, exc)) + + def run_luks_add_key(self, device, keyfile, passphrase, new_keyfile, + new_passphrase, pbkdf): + ''' 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 pbkdf is not None: + self._add_pbkdf_options(args, pbkdf) + + 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 `true`." % 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])) + + def luks_test_key(self, device, keyfile, passphrase): + ''' Check whether the keyfile or passphrase works. + Raises ValueError when command fails. + ''' + data = None + args = [self._cryptsetup_bin, 'luksOpen', '--test-passphrase', device] + + if keyfile: + args.extend(['--key-file', keyfile]) + else: + data = passphrase + + result = self._run_command(args, data=data) + if result[RETURN_CODE] == 0: + return True + for output in (STDOUT, STDERR): + if 'No key available with this passphrase' in result[output]: + return False + + raise ValueError('Error while testing whether keyslot exists on %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 not self._crypthandler.luks_test_key(self.device, self._module.params['new_keyfile'], self._module.params['new_passphrase']) + + 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 self._crypthandler.luks_test_key(self.device, self._module.params['remove_keyfile'], self._module.params['remove_passphrase']) + + 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']), + cipher=dict(type='str'), + hash=dict(type='str'), + pbkdf=dict( + type='dict', + options=dict( + iteration_time=dict(type='float'), + iteration_count=dict(type='int'), + algorithm=dict(type='str', choices=['argon2i', 'argon2id', 'pbkdf2']), + memory=dict(type='int'), + parallel=dict(type='int'), + ), + mutually_exclusive=[('iteration_time', 'iteration_count')], + ), + sector_size=dict(type='int'), + perf_same_cpu_crypt=dict(type='bool', default=False), + perf_submit_from_crypt_cpus=dict(type='bool', default=False), + perf_no_read_workqueue=dict(type='bool', default=False), + perf_no_write_workqueue=dict(type='bool', default=False), + persistent=dict(type='bool', default=False), + ) + + 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) + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + + 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'], + module.params['cipher'], + module.params['hash'], + module.params['sector_size'], + module.params['pbkdf'], + ) + 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'], + module.params['perf_same_cpu_crypt'], + module.params['perf_submit_from_crypt_cpus'], + module.params['perf_no_read_workqueue'], + module.params['perf_no_write_workqueue'], + module.params['persistent'], + 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'], + module.params['pbkdf']) + 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() diff --git a/ansible_collections/community/crypto/plugins/modules/openssh_cert.py b/ansible_collections/community/crypto/plugins/modules/openssh_cert.py new file mode 100644 index 000000000..8f428107a --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssh_cert.py @@ -0,0 +1,578 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: openssh_cert +author: "David Kainz (@lolcube)" +short_description: Generate OpenSSH host or user certificates. +description: + - Generate and regenerate OpenSSH host or user certificates. +requirements: + - "ssh-keygen" +extends_documentation_fragment: + - ansible.builtin.files + - community.crypto.attributes + - community.crypto.attributes.files +attributes: + check_mode: + support: full + diff_mode: + support: full + safe_file_operations: + support: full +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. + - Equivalent to I(regenerate=always). + type: bool + default: false + path: + description: + - Path of the file containing the certificate. + type: path + required: true + regenerate: + description: + - When C(never) the task will fail if a certificate already exists at I(path) and is unreadable + otherwise a new certificate will only be generated if there is no existing certificate. + - When C(fail) the task will fail if a certificate already exists at I(path) and does not + match the module's options. + - When C(partial_idempotence) an existing certificate will be regenerated based on + I(serial), I(signature_algorithm), I(type), I(valid_from), I(valid_to), I(valid_at), and I(principals). + I(valid_from) and I(valid_to) can be excluded by I(ignore_timestamps=true). + - When C(full_idempotence) I(identifier), I(options), I(public_key), and I(signing_key) + are also considered when compared against an existing certificate. + - C(always) is equivalent to I(force=true). + type: str + choices: + - never + - fail + - partial_idempotence + - full_idempotence + - always + default: partial_idempotence + version_added: 1.8.0 + signature_algorithm: + description: + - As of OpenSSH 8.2 the SHA-1 signature algorithm for RSA keys has been disabled and C(ssh) will refuse + host certificates signed with the SHA-1 algorithm. OpenSSH 8.1 made C(rsa-sha2-512) the default algorithm + when acting as a CA and signing certificates with a RSA key. However, for OpenSSH versions less than 8.1 + the SHA-2 signature algorithms, C(rsa-sha2-256) or C(rsa-sha2-512), must be specified using this option + if compatibility with newer C(ssh) clients is required. Conversely if hosts using OpenSSH version 8.2 + or greater must remain compatible with C(ssh) clients using OpenSSH less than 7.2, then C(ssh-rsa) + can be used when generating host certificates (a corresponding change to the sshd_config to add C(ssh-rsa) + to the C(CASignatureAlgorithms) keyword is also required). + - Using any value for this option with a non-RSA I(signing_key) will cause this module to fail. + - "Note: OpenSSH versions prior to 7.2 do not support SHA-2 signature algorithms for RSA keys and OpenSSH + versions prior to 7.3 do not support SHA-2 signature algorithms for certificates." + - See U(https://www.openssh.com/txt/release-8.2) for more information. + type: str + choices: + - ssh-rsa + - rsa-sha2-256 + - rsa-sha2-512 + version_added: 1.10.0 + signing_key: + description: + - The path to the private openssh key that is used for signing the public key in order to generate the certificate. + - If the private key is on a PKCS#11 token (I(pkcs11_provider)), set this to the path to the public key instead. + - Required if I(state) is C(present). + type: path + pkcs11_provider: + description: + - To use a signing key that resides on a PKCS#11 token, set this to the name (or full path) of the shared library to use with the token. + Usually C(libpkcs11.so). + - If this is set, I(signing_key) needs to point to a file containing the public key of the CA. + type: str + version_added: 1.1.0 + use_agent: + description: + - Should the ssh-keygen use a CA key residing in a ssh-agent. + type: bool + default: false + version_added: 1.3.0 + 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]) (for example C(+32w1d2h)). + Note that if using relative time this module is NOT idempotent." + - "The value C(always) is only supported for OpenSSH 7.7 and greater, however, the value C(1970-01-01T00:00:01) + can be used with earlier versions as an equivalent expression." + - "To ignore this value during comparison with an existing certificate set I(ignore_timestamps=true)." + - 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]) (for example C(+32w1d2h)). + Note that if using relative time this module is NOT idempotent." + - "To ignore this value during comparison with an existing certificate set I(ignore_timestamps=true)." + - 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 + ignore_timestamps: + description: + - "Whether the I(valid_from) and I(valid_to) timestamps should be ignored for idempotency checks." + - "However, the values will still be applied to a new certificate if it meets any other necessary conditions for generation/regeneration." + type: bool + default: false + version_added: 2.2.0 + 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 +''' + +EXAMPLES = ''' +- name: Generate an OpenSSH user certificate that is valid forever and for all users + community.crypto.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 +- name: Generate an OpenSSH host certificate with valid_from, valid_to and valid_at parameters + community.crypto.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 + ignore_timestamps: true + +- name: Generate an OpenSSH host certificate that is valid forever and only for example.com and examplehost + community.crypto.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 + +- name: Generate an OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019 + community.crypto.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" + +- name: Generate an OpenSSH user Certificate with clear and force-command option + community.crypto.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" + +- name: Generate an OpenSSH user certificate using a PKCS#11 token + community.crypto.openssh_cert: + type: user + signing_key: /path/to/ca_public_key.pub + pkcs11_provider: libpkcs11.so + public_key: /path/to/public_key.pub + path: /path/to/certificate + valid_from: always + valid_to: forever + +''' + +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 + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import ( + KeygenCommand, + OpensshModule, + PrivateKey, +) + +from ansible_collections.community.crypto.plugins.module_utils.openssh.certificate import ( + OpensshCertificate, + OpensshCertificateTimeParameters, + parse_option_list, +) + + +class Certificate(OpensshModule): + def __init__(self, module): + super(Certificate, self).__init__(module) + self.ssh_keygen = KeygenCommand(self.module) + + self.identifier = self.module.params['identifier'] or "" + self.options = self.module.params['options'] or [] + self.path = self.module.params['path'] + self.pkcs11_provider = self.module.params['pkcs11_provider'] + self.principals = self.module.params['principals'] or [] + self.public_key = self.module.params['public_key'] + self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always' + self.serial_number = self.module.params['serial_number'] + self.signature_algorithm = self.module.params['signature_algorithm'] + self.signing_key = self.module.params['signing_key'] + self.state = self.module.params['state'] + self.type = self.module.params['type'] + self.use_agent = self.module.params['use_agent'] + self.valid_at = self.module.params['valid_at'] + self.ignore_timestamps = self.module.params['ignore_timestamps'] + + self._check_if_base_dir(self.path) + + if self.state == 'present': + self._validate_parameters() + + self.data = None + self.original_data = None + if self._exists(): + self._load_certificate() + + self.time_parameters = None + if self.state == 'present': + self._set_time_parameters() + + def _validate_parameters(self): + for path in (self.public_key, self.signing_key): + self._check_if_base_dir(path) + + if self.options and self.type == "host": + self.module.fail_json(msg="Options can only be used with user certificates.") + + if self.use_agent: + self._use_agent_available() + + def _use_agent_available(self): + ssh_version = self._get_ssh_version() + if not ssh_version: + self.module.fail_json(msg="Failed to determine ssh version") + elif LooseVersion(ssh_version) < LooseVersion("7.6"): + self.module.fail_json( + msg="Signing with CA key in ssh agent requires ssh 7.6 or newer." + + " Your version is: %s" % ssh_version + ) + + def _exists(self): + return os.path.exists(self.path) + + def _load_certificate(self): + try: + self.original_data = OpensshCertificate.load(self.path) + except (TypeError, ValueError) as e: + if self.regenerate in ('never', 'fail'): + self.module.fail_json(msg="Unable to read existing certificate: %s" % to_native(e)) + self.module.warn("Unable to read existing certificate: %s" % to_native(e)) + + def _set_time_parameters(self): + try: + self.time_parameters = OpensshCertificateTimeParameters( + valid_from=self.module.params['valid_from'], + valid_to=self.module.params['valid_to'], + ) + except ValueError as e: + self.module.fail_json(msg=to_native(e)) + + def _execute(self): + if self.state == 'present': + if self._should_generate(): + self._generate() + self._update_permissions(self.path) + else: + if self._exists(): + self._remove() + + def _should_generate(self): + if self.regenerate == 'never': + return self.original_data is None + elif self.regenerate == 'fail': + if self.original_data and not self._is_fully_valid(): + self.module.fail_json( + msg="Certificate does not match the provided options.", + cert=get_cert_dict(self.original_data) + ) + return self.original_data is None + elif self.regenerate == 'partial_idempotence': + return self.original_data is None or not self._is_partially_valid() + elif self.regenerate == 'full_idempotence': + return self.original_data is None or not self._is_fully_valid() + else: + return True + + def _is_fully_valid(self): + return self._is_partially_valid() and all([ + self._compare_options() if self.original_data.type == 'user' else True, + self.original_data.key_id == self.identifier, + self.original_data.public_key == self._get_key_fingerprint(self.public_key), + self.original_data.signing_key == self._get_key_fingerprint(self.signing_key), + ]) + + def _is_partially_valid(self): + return all([ + set(self.original_data.principals) == set(self.principals), + self.original_data.signature_type == self.signature_algorithm if self.signature_algorithm else True, + self.original_data.serial == self.serial_number if self.serial_number is not None else True, + self.original_data.type == self.type, + self._compare_time_parameters(), + ]) + + def _compare_time_parameters(self): + try: + original_time_parameters = OpensshCertificateTimeParameters( + valid_from=self.original_data.valid_after, + valid_to=self.original_data.valid_before + ) + except ValueError as e: + return self.module.fail_json(msg=to_native(e)) + + if self.ignore_timestamps: + return original_time_parameters.within_range(self.valid_at) + + return all([ + original_time_parameters == self.time_parameters, + original_time_parameters.within_range(self.valid_at) + ]) + + def _compare_options(self): + try: + critical_options, extensions = parse_option_list(self.options) + except ValueError as e: + return self.module.fail_json(msg=to_native(e)) + + return all([ + set(self.original_data.critical_options) == set(critical_options), + set(self.original_data.extensions) == set(extensions) + ]) + + def _get_key_fingerprint(self, path): + private_key_content = self.ssh_keygen.get_private_key(path, check_rc=True)[1] + return PrivateKey.from_string(private_key_content).fingerprint + + @OpensshModule.trigger_change + @OpensshModule.skip_if_check_mode + def _generate(self): + try: + temp_certificate = self._generate_temp_certificate() + self._safe_secure_move([(temp_certificate, self.path)]) + except OSError as e: + self.module.fail_json(msg="Unable to write certificate to %s: %s" % (self.path, to_native(e))) + + try: + self.data = OpensshCertificate.load(self.path) + except (TypeError, ValueError) as e: + self.module.fail_json(msg="Unable to read new certificate: %s" % to_native(e)) + + def _generate_temp_certificate(self): + key_copy = os.path.join(self.module.tmpdir, os.path.basename(self.public_key)) + + try: + self.module.preserved_copy(self.public_key, key_copy) + except OSError as e: + self.module.fail_json(msg="Unable to stage temporary key: %s" % to_native(e)) + self.module.add_cleanup_file(key_copy) + + self.ssh_keygen.generate_certificate( + key_copy, self.identifier, self.options, self.pkcs11_provider, self.principals, self.serial_number, + self.signature_algorithm, self.signing_key, self.type, self.time_parameters, self.use_agent, + environ_update=dict(TZ="UTC"), check_rc=True + ) + + temp_cert = os.path.splitext(key_copy)[0] + '-cert.pub' + self.module.add_cleanup_file(temp_cert) + + return temp_cert + + @OpensshModule.trigger_change + @OpensshModule.skip_if_check_mode + def _remove(self): + try: + os.remove(self.path) + except OSError as e: + self.module.fail_json(msg="Unable to remove existing certificate: %s" % to_native(e)) + + @property + def _result(self): + if self.state != 'present': + return {} + + certificate_info = self.ssh_keygen.get_certificate_info(self.path)[1] + + return { + 'type': self.type, + 'filename': self.path, + 'info': format_cert_info(certificate_info), + } + + @property + def diff(self): + return { + 'before': get_cert_dict(self.original_data), + 'after': get_cert_dict(self.data) + } + + +def format_cert_info(cert_info): + result = [] + string = "" + + for word in cert_info.split(): + if word in ("Type:", "Public", "Signing", "Key", "Serial:", "Valid:", "Principals:", "Critical", "Extensions:"): + result.append(string) + string = word + else: + string += " " + word + result.append(string) + # Drop the certificate path + result.pop(0) + return result + + +def get_cert_dict(data): + if data is None: + return {} + + result = data.to_dict() + result.pop('nonce') + result['signature_algorithm'] = data.signature_type + + return result + + +def main(): + module = AnsibleModule( + argument_spec=dict( + force=dict(type='bool', default=False), + identifier=dict(type='str'), + options=dict(type='list', elements='str'), + path=dict(type='path', required=True), + pkcs11_provider=dict(type='str'), + principals=dict(type='list', elements='str'), + public_key=dict(type='path'), + regenerate=dict( + type='str', + default='partial_idempotence', + choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always'] + ), + signature_algorithm=dict(type='str', choices=['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512']), + signing_key=dict(type='path'), + serial_number=dict(type='int'), + state=dict(type='str', default='present', choices=['absent', 'present']), + type=dict(type='str', choices=['host', 'user']), + use_agent=dict(type='bool', default=False), + valid_at=dict(type='str'), + valid_from=dict(type='str'), + valid_to=dict(type='str'), + ignore_timestamps=dict(type='bool', default=False), + ), + supports_check_mode=True, + add_file_common_args=True, + required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])], + ) + + Certificate(module).execute() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/openssh_keypair.py b/ansible_collections/community/crypto/plugins/modules/openssh_keypair.py new file mode 100644 index 000000000..35ee6d631 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssh_keypair.py @@ -0,0 +1,244 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: openssh_keypair +author: "David Kainz (@lolcube)" +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 (if I(backend=openssh)) + - cryptography >= 2.6 (if I(backend=cryptography) and OpenSSH < 7.8 is installed) + - cryptography >= 3.0 (if I(backend=cryptography) and OpenSSH >= 7.8 is installed) +extends_documentation_fragment: + - ansible.builtin.files + - community.crypto.attributes + - community.crypto.attributes.files +attributes: + check_mode: + support: full + diff_mode: + support: full + safe_file_operations: + support: full +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. + type: str + passphrase: + description: + - Passphrase used to decrypt an existing private key or encrypt a newly generated private key. + - Passphrases are not supported for I(type=rsa1). + - Can only be used when I(backend=cryptography), or when I(backend=auto) and a required C(cryptography) version is installed. + type: str + version_added: 1.7.0 + private_key_format: + description: + - Used when I(backend=cryptography) to select a format for the private key at the provided I(path). + - When set to C(auto) this module will match the key format of the installed OpenSSH version. + - For OpenSSH < 7.8 private keys will be in PKCS1 format except ed25519 keys which will be in OpenSSH format. + - For OpenSSH >= 7.8 all private key types will be in the OpenSSH format. + - Using this option when I(regenerate=partial_idempotence) or I(regenerate=full_idempotence) will cause + a new keypair to be generated if the private key's format does not match the value of I(private_key_format). + This module will not however convert existing private keys between formats. + type: str + default: auto + choices: + - auto + - pkcs1 + - pkcs8 + - ssh + version_added: 1.7.0 + backend: + description: + - Selects between the C(cryptography) library or the OpenSSH binary C(opensshbin). + - C(auto) will default to C(opensshbin) unless the OpenSSH binary is not installed or when using I(passphrase). + type: str + default: auto + choices: + - auto + - cryptography + - opensshbin + version_added: 1.7.0 + 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 does not 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 + is not 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(true). + - 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: '1.0.0' +notes: + - In case the ssh key is broken or password protected, the module will fail. + Set the I(force) option to C(true) if you want to regenerate the keypair. + - In the case a custom C(mode), C(group), C(owner), or other file attribute is provided it will be applied to both key files. +''' + +EXAMPLES = ''' +- name: Generate an OpenSSH keypair with the default values (4096 bits, rsa) + community.crypto.openssh_keypair: + path: /tmp/id_ssh_rsa + +- name: Generate an OpenSSH keypair with the default values (4096 bits, rsa) and encrypted private key + community.crypto.openssh_keypair: + path: /tmp/id_ssh_rsa + passphrase: super_secret_password + +- name: Generate an OpenSSH rsa keypair with a different size (2048 bits) + community.crypto.openssh_keypair: + path: /tmp/id_ssh_rsa + size: 2048 + +- name: Force regenerate an OpenSSH keypair if it already exists + community.crypto.openssh_keypair: + path: /tmp/id_ssh_rsa + force: true + +- name: Generate an OpenSSH keypair with a different algorithm (dsa) + community.crypto.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== +comment: + description: The comment of the generated key. + returned: changed or success + type: str + sample: test@comment +''' + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.keypair_backend import ( + select_backend +) + + +def main(): + + 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'] + ), + passphrase=dict(type='str', no_log=True), + private_key_format=dict( + type='str', + default='auto', + no_log=False, + choices=['auto', 'pkcs1', 'pkcs8', 'ssh']), + backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'opensshbin']) + ), + supports_check_mode=True, + add_file_common_args=True, + ) + + keypair = select_backend(module, module.params['backend'])[1] + + keypair.execute() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_csr.py b/ansible_collections/community/crypto/plugins/modules/openssl_csr.py new file mode 100644 index 000000000..69b663b23 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_csr.py @@ -0,0 +1,359 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017, Yanis Guenane <yanis+ansible@guenane.org> +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_csr +short_description: Generate OpenSSL Certificate Signing Request (CSR) +description: + - "Please note that the module regenerates an existing CSR if it does not match the module's + options, or if it seems to be corrupt. If you are concerned that this could overwrite + your existing CSR, consider using the I(backup) option." +author: + - Yanis Guenane (@Spredzy) + - Felix Fontein (@felixfontein) +extends_documentation_fragment: + - ansible.builtin.files + - community.crypto.attributes + - community.crypto.attributes.files + - community.crypto.module_csr +attributes: + check_mode: + support: full + diff_mode: + support: full + safe_file_operations: + support: full +options: + state: + description: + - Whether the certificate signing request 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 certificate signing request be forced regenerated by this ansible module. + type: bool + default: false + path: + description: + - The name of the file into which the generated OpenSSL certificate signing request will be written. + type: path + required: true + backup: + description: + - Create a backup file including a timestamp so you can get the original + CSR back if you overwrote it with a new one by accident. + type: bool + default: false + return_content: + description: + - If set to C(true), will return the (current or generated) CSR's content as I(csr). + type: bool + default: false + version_added: "1.0.0" + privatekey_content: + version_added: "1.0.0" + name_constraints_permitted: + version_added: 1.1.0 + name_constraints_excluded: + version_added: 1.1.0 + name_constraints_critical: + version_added: 1.1.0 +seealso: + - module: community.crypto.openssl_csr_pipe +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL Certificate Signing Request + community.crypto.openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + common_name: www.ansible.com + +- name: Generate an OpenSSL Certificate Signing Request with an inline key + community.crypto.openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_content: "{{ private_key_content }}" + common_name: www.ansible.com + +- name: Generate an OpenSSL Certificate Signing Request with a passphrase protected private key + community.crypto.openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + privatekey_passphrase: ansible + common_name: www.ansible.com + +- name: Generate an OpenSSL Certificate Signing Request with Subject information + community.crypto.openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + country_name: FR + organization_name: Ansible + email_address: jdoe@ansible.com + common_name: www.ansible.com + +- name: Generate an OpenSSL Certificate Signing Request with subjectAltName extension + community.crypto.openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + subject_alt_name: 'DNS:www.ansible.com,DNS:m.ansible.com' + +- name: Generate an OpenSSL CSR with subjectAltName extension with dynamic list + community.crypto.openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + subject_alt_name: "{{ item.value | map('regex_replace', '^', 'DNS:') | list }}" + with_dict: + dns_server: + - www.ansible.com + - m.ansible.com + +- name: Force regenerate an OpenSSL Certificate Signing Request + community.crypto.openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + force: true + common_name: www.ansible.com + +- name: Generate an OpenSSL Certificate Signing Request with special key usages + community.crypto.openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + common_name: www.ansible.com + key_usage: + - digitalSignature + - keyAgreement + extended_key_usage: + - clientAuth + +- name: Generate an OpenSSL Certificate Signing Request with OCSP Must Staple + community.crypto.openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + common_name: www.ansible.com + ocsp_must_staple: true + +- name: Generate an OpenSSL Certificate Signing Request for WinRM Certificate authentication + community.crypto.openssl_csr: + path: /etc/ssl/csr/winrm.auth.csr + privatekey_path: /etc/ssl/private/winrm.auth.pem + common_name: username + extended_key_usage: + - clientAuth + subject_alt_name: otherName:1.3.6.1.4.1.311.20.2.3;UTF8:username@localhost + +- name: Generate an OpenSSL Certificate Signing Request with a CRL distribution point + community.crypto.openssl_csr: + path: /etc/ssl/csr/www.ansible.com.csr + privatekey_path: /etc/ssl/private/ansible.com.pem + common_name: www.ansible.com + crl_distribution_points: + - full_name: + - "URI:https://ca.example.com/revocations.crl" + crl_issuer: + - "URI:https://ca.example.com/" + reasons: + - key_compromise + - ca_compromise + - cessation_of_operation +''' + +RETURN = r''' +privatekey: + description: + - Path to the TLS/SSL private key the CSR was generated for + - 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 +filename: + description: Path to the generated Certificate Signing Request + returned: changed or success + type: str + sample: /etc/ssl/csr/www.ansible.com.csr +subject: + description: A list of the subject tuples attached to the CSR + returned: changed or success + type: list + elements: list + sample: [['CN', 'www.ansible.com'], ['O', 'Ansible']] +subjectAltName: + description: The alternative names this CSR is valid for + returned: changed or success + type: list + elements: str + sample: [ 'DNS:www.ansible.com', 'DNS:m.ansible.com' ] +keyUsage: + description: Purpose for which the public key may be used + returned: changed or success + type: list + elements: str + sample: [ 'digitalSignature', 'keyAgreement' ] +extendedKeyUsage: + description: Additional restriction on the public key purposes + returned: changed or success + type: list + elements: str + sample: [ 'clientAuth' ] +basicConstraints: + description: Indicates if the certificate belongs to a CA + returned: changed or success + type: list + elements: str + sample: ['CA:TRUE', 'pathLenConstraint:0'] +ocsp_must_staple: + description: Indicates whether the certificate has the OCSP + Must Staple feature enabled + returned: changed or success + type: bool + sample: false +name_constraints_permitted: + description: List of permitted subtrees to sign certificates for. + returned: changed or success + type: list + elements: str + sample: ['email:.somedomain.com'] + version_added: 1.1.0 +name_constraints_excluded: + description: List of excluded subtrees the CA cannot sign certificates for. + returned: changed or success + type: list + elements: str + sample: ['email:.com'] + version_added: 1.1.0 +backup_file: + description: Name of backup file created. + returned: changed and if I(backup) is C(true) + type: str + sample: /path/to/www.ansible.com.csr.2019-03-09@11:22~ +csr: + description: The (current or generated) CSR's content. + returned: if I(state) is C(present) and I(return_content) is C(true) + type: str + version_added: "1.0.0" +''' + +import os + +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr import ( + select_backend, + get_csr_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, +) + + +class CertificateSigningRequestModule(OpenSSLObject): + + def __init__(self, module, module_backend): + super(CertificateSigningRequestModule, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode + ) + self.module_backend = module_backend + self.return_content = module.params['return_content'] + + self.backup = module.params['backup'] + self.backup_file = None + + self.module_backend.set_existing(load_file_if_exists(self.path, module)) + + def generate(self, module): + '''Generate the certificate signing request.''' + if self.force or self.module_backend.needs_regeneration(): + if not self.check_mode: + self.module_backend.generate_csr() + result = self.module_backend.get_csr_data() + if self.backup: + self.backup_file = module.backup_local(self.path) + write_file(module, result) + self.changed = True + + file_args = module.load_file_common_arguments(module.params) + if module.check_file_absent_if_check_mode(file_args['path']): + self.changed = True + else: + self.changed = module.set_fs_attributes_if_different(file_args, self.changed) + + def remove(self, module): + self.module_backend.set_existing(None) + if self.backup and not self.check_mode: + self.backup_file = module.backup_local(self.path) + super(CertificateSigningRequestModule, self).remove(module) + + def dump(self): + '''Serialize the object into a dictionary.''' + result = self.module_backend.dump(include_csr=self.return_content) + result.update({ + 'filename': self.path, + 'changed': self.changed, + }) + if self.backup_file: + result['backup_file'] = self.backup_file + return result + + +def main(): + argument_spec = get_csr_argument_spec() + argument_spec.argument_spec.update(dict( + state=dict(type='str', default='present', choices=['absent', 'present']), + force=dict(type='bool', default=False), + path=dict(type='path', required=True), + backup=dict(type='bool', default=False), + return_content=dict(type='bool', default=False), + )) + argument_spec.required_if.extend([('state', 'present', rof, True) for rof in argument_spec.required_one_of]) + argument_spec.required_one_of = [] + module = argument_spec.create_ansible_module( + add_file_common_args=True, + supports_check_mode=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) + + try: + backend = module.params['select_crypto_backend'] + backend, module_backend = select_backend(module, backend) + + csr = CertificateSigningRequestModule(module, module_backend) + if module.params['state'] == 'present': + csr.generate(module) + else: + csr.remove(module) + + result = csr.dump() + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_csr_info.py b/ansible_collections/community/crypto/plugins/modules/openssl_csr_info.py new file mode 100644 index 000000000..1ef07e733 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_csr_info.py @@ -0,0 +1,359 @@ +#!/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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_csr_info +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 cryptography python library to interact with OpenSSL. +requirements: + - cryptography >= 1.3 +author: + - Felix Fontein (@felixfontein) + - Yanis Guenane (@Spredzy) +extends_documentation_fragment: + - community.crypto.attributes + - community.crypto.attributes.info_module + - community.crypto.name_encoding +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: "1.0.0" + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography ] + +seealso: + - module: community.crypto.openssl_csr + - module: community.crypto.openssl_csr_pipe + - ref: community.crypto.openssl_csr_info filter <ansible_collections.community.crypto.openssl_csr_info_filter> + # - plugin: community.crypto.openssl_csr_info + # plugin_type: filter + description: A filter variant of this module. +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL Certificate Signing Request + community.crypto.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 + community.crypto.openssl_csr_info: + path: /etc/ssl/csr/www.ansible.com.csr + register: result + +- name: Dump information + ansible.builtin.debug: + var: result +''' + +RETURN = r''' +signature_valid: + description: + - Whether the CSR's signature is valid. + - In case the check returns C(false), 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. + - B(Note) that depending on the C(cryptography) version used, it is + not possible to extract the ASN.1 content of the extension, but only + to provide the re-encoded content of the extension in case it was + parsed by C(cryptography). This should usually result in exactly the + same value, except if the original extension value was malformed. + 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. + - See I(name_encoding) for how IDNs are handled. + 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(true) 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 +name_constraints_permitted: + description: List of permitted subtrees to sign certificates for. + returned: success + type: list + elements: str + sample: ['email:.somedomain.com'] + version_added: 1.1.0 +name_constraints_excluded: + description: + - List of excluded subtrees the CA cannot sign certificates for. + - Is C(none) if extension is not present. + - See I(name_encoding) for how IDNs are handled. + returned: success + type: list + elements: str + sample: ['email:.com'] + version_added: 1.1.0 +name_constraints_critical: + description: + - Whether the C(name_constraints) extension is critical. + - Is C(none) if extension is not present. + returned: success + type: bool + version_added: 1.1.0 +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"]] +public_key: + description: CSR's public key in PEM format + returned: success + type: str + sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..." +public_key_type: + description: + - The CSR's public 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 + version_added: 1.7.0 + sample: RSA +public_key_data: + description: + - Public key data. Depends on the public key's type. + returned: success + type: dict + version_added: 1.7.0 + contains: + size: + description: + - Bit size of modulus (RSA) or prime number (DSA). + type: int + returned: When C(public_key_type=RSA) or C(public_key_type=DSA) + modulus: + description: + - The RSA key's modulus. + type: int + returned: When C(public_key_type=RSA) + exponent: + description: + - The RSA key's public exponent. + type: int + returned: When C(public_key_type=RSA) + p: + description: + - The C(p) value for DSA. + - This is the prime modulus upon which arithmetic takes place. + type: int + returned: When C(public_key_type=DSA) + q: + description: + - The C(q) value for DSA. + - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the + multiplicative group of the prime field used. + type: int + returned: When C(public_key_type=DSA) + g: + description: + - The C(g) value for DSA. + - This is the element spanning the subgroup of the multiplicative group of the prime field used. + type: int + returned: When C(public_key_type=DSA) + curve: + description: + - The curve's name for ECC. + type: str + returned: When C(public_key_type=ECC) + exponent_size: + description: + - The maximum number of bits of a private key. This is basically the bit size of the subgroup used. + type: int + returned: When C(public_key_type=ECC) + x: + description: + - The C(x) coordinate for the public point on the elliptic curve. + type: int + returned: When C(public_key_type=ECC) + y: + description: + - For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. + - For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. + type: int + returned: When C(public_key_type=DSA) or C(public_key_type=ECC) +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 + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' +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 + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' +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. + - See I(name_encoding) for how IDNs are handled. + returned: success + type: list + elements: str + sample: ["DNS:www.ansible.com", "IP:1.2.3.4"] +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 + type: int + sample: 12345 +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import ( + select_backend, +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + path=dict(type='path'), + content=dict(type='str'), + name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']), + ), + required_one_of=( + ['path', 'content'], + ), + mutually_exclusive=( + ['path', 'content'], + ), + supports_check_mode=True, + ) + + if module.params['content'] is not None: + data = module.params['content'].encode('utf-8') + else: + try: + with open(module.params['path'], 'rb') as f: + data = f.read() + except (IOError, OSError) as e: + module.fail_json(msg='Error while reading CSR file from disk: {0}'.format(e)) + + backend, module_backend = select_backend(module, module.params['select_crypto_backend'], data, validate_signature=True) + + try: + result = module_backend.get_info() + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_csr_pipe.py b/ansible_collections/community/crypto/plugins/modules/openssl_csr_pipe.py new file mode 100644 index 000000000..66cc67354 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_csr_pipe.py @@ -0,0 +1,184 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017, Yanis Guenane <yanis+ansible@guenane.org> +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_csr_pipe +short_description: Generate OpenSSL Certificate Signing Request (CSR) +version_added: 1.3.0 +description: + - "Please note that the module regenerates an existing CSR if it does not match the module's + options, or if it seems to be corrupt." +author: + - Yanis Guenane (@Spredzy) + - Felix Fontein (@felixfontein) +extends_documentation_fragment: + - community.crypto.attributes + - community.crypto.module_csr +attributes: + check_mode: + support: full + diff_mode: + support: full +options: + content: + description: + - The existing CSR. + type: str +seealso: +- module: community.crypto.openssl_csr +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL Certificate Signing Request + community.crypto.openssl_csr_pipe: + privatekey_path: /etc/ssl/private/ansible.com.pem + common_name: www.ansible.com + register: result +- name: Print CSR + ansible.builtin.debug: + var: result.csr + +- name: Generate an OpenSSL Certificate Signing Request with an inline CSR + community.crypto.openssl_csr: + content: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.csr') }}" + privatekey_content: "{{ private_key_content }}" + common_name: www.ansible.com + register: result +- name: Store CSR + ansible.builtin.copy: + dest: /etc/ssl/csr/www.ansible.com.csr + content: "{{ result.csr }}" + when: result is changed +''' + +RETURN = r''' +privatekey: + description: + - Path to the TLS/SSL private key the CSR was generated for + - 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 +subject: + description: A list of the subject tuples attached to the CSR + returned: changed or success + type: list + elements: list + sample: [['CN', 'www.ansible.com'], ['O', 'Ansible']] +subjectAltName: + description: The alternative names this CSR is valid for + returned: changed or success + type: list + elements: str + sample: [ 'DNS:www.ansible.com', 'DNS:m.ansible.com' ] +keyUsage: + description: Purpose for which the public key may be used + returned: changed or success + type: list + elements: str + sample: [ 'digitalSignature', 'keyAgreement' ] +extendedKeyUsage: + description: Additional restriction on the public key purposes + returned: changed or success + type: list + elements: str + sample: [ 'clientAuth' ] +basicConstraints: + description: Indicates if the certificate belongs to a CA + returned: changed or success + type: list + elements: str + sample: ['CA:TRUE', 'pathLenConstraint:0'] +ocsp_must_staple: + description: Indicates whether the certificate has the OCSP + Must Staple feature enabled + returned: changed or success + type: bool + sample: false +name_constraints_permitted: + description: List of permitted subtrees to sign certificates for. + returned: changed or success + type: list + elements: str + sample: ['email:.somedomain.com'] +name_constraints_excluded: + description: List of excluded subtrees the CA cannot sign certificates for. + returned: changed or success + type: list + elements: str + sample: ['email:.com'] +csr: + description: The (current or generated) CSR's content. + returned: changed or success + type: str +''' + +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr import ( + select_backend, + get_csr_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + + +class CertificateSigningRequestModule(object): + def __init__(self, module, module_backend): + self.check_mode = module.check_mode + self.module_backend = module_backend + self.changed = False + if module.params['content'] is not None: + self.module_backend.set_existing(module.params['content'].encode('utf-8')) + + def generate(self, module): + '''Generate the certificate signing request.''' + if self.module_backend.needs_regeneration(): + if not self.check_mode: + self.module_backend.generate_csr() + self.changed = True + + def dump(self): + '''Serialize the object into a dictionary.''' + result = self.module_backend.dump(include_csr=True) + result.update({ + 'changed': self.changed, + }) + return result + + +def main(): + argument_spec = get_csr_argument_spec() + argument_spec.argument_spec.update(dict( + content=dict(type='str'), + )) + module = argument_spec.create_ansible_module( + supports_check_mode=True, + ) + + try: + backend = module.params['select_crypto_backend'] + backend, module_backend = select_backend(module, backend) + + csr = CertificateSigningRequestModule(module, module_backend) + csr.generate(module) + result = csr.dump() + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_dhparam.py b/ansible_collections/community/crypto/plugins/modules/openssl_dhparam.py new file mode 100644 index 000000000..d9e1e982e --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_dhparam.py @@ -0,0 +1,431 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017, Thom Wiggers <ansible@thomwiggers.nl> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_dhparam +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 do not + 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) +extends_documentation_fragment: + - ansible.builtin.files + - community.crypto.attributes + - community.crypto.attributes.files +attributes: + check_mode: + support: full + diff_mode: + support: none + safe_file_operations: + support: full +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: false + 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: false + 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: "1.0.0" + return_content: + description: + - If set to C(true), will return the (current or generated) DH parameter's content as I(dhparams). + type: bool + default: false + version_added: "1.0.0" +seealso: + - module: community.crypto.x509_certificate + - module: community.crypto.openssl_csr + - module: community.crypto.openssl_pkcs12 + - module: community.crypto.openssl_privatekey + - module: community.crypto.openssl_publickey +''' + +EXAMPLES = r''' +- name: Generate Diffie-Hellman parameters with the default size (4096 bits) + community.crypto.openssl_dhparam: + path: /etc/ssl/dhparams.pem + +- name: Generate DH Parameters with a different size (2048 bits) + community.crypto.openssl_dhparam: + path: /etc/ssl/dhparams.pem + size: 2048 + +- name: Force regenerate an DH parameters if they already exist + community.crypto.openssl_dhparam: + path: /etc/ssl/dhparams.pem + force: true +''' + +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(true) + 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(true) + type: str + version_added: "1.0.0" +''' + +import abc +import os +import re +import tempfile +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.math import ( + count_bits, +) + +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) + if module.check_file_absent_if_check_mode(file_args['path']): + return False + return not module.set_fs_attributes_if_different(file_args, False) + + 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 = 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 does not 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) + 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 = 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=("Cannot 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() diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_pkcs12.py b/ansible_collections/community/crypto/plugins/modules/openssl_pkcs12.py new file mode 100644 index 000000000..e74553b58 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_pkcs12.py @@ -0,0 +1,848 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017, Guillaume Delpierre <gde@llew.me> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_pkcs12 +author: +- Guillaume Delpierre (@gdelpierre) +short_description: Generate OpenSSL PKCS#12 archive +description: + - This module allows one to (re-)generate PKCS#12. + - The module can use the cryptography Python library, or the pyOpenSSL Python + library. By default, it tries to detect which one is available, assuming none of the + I(iter_size) and I(maciter_size) options are used. This can be overridden with the + I(select_crypto_backend) option. + # Please note that the C(pyopenssl) backend has been deprecated in community.crypto x.y.0, + # and will be removed in community.crypto (x+1).0.0. +requirements: + - PyOpenSSL >= 0.15 or cryptography >= 3.0 +extends_documentation_fragment: + - ansible.builtin.files + - community.crypto.attributes + - community.crypto.attributes.files +attributes: + check_mode: + support: full + diff_mode: + support: none + safe_file_operations: + support: full +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 Ansible 2.8 this parameter was called I(ca_certificates). + - Assumes there is one PEM-encoded certificate per file. If a file contains multiple PEM certificates, + set I(other_certificates_parse_all) to C(true). + type: list + elements: path + aliases: [ ca_certificates ] + other_certificates_parse_all: + description: + - If set to C(true), assumes that the files mentioned in I(other_certificates) can contain more than one + certificate per file (or even none per file). + type: bool + default: false + version_added: 1.4.0 + 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: false + 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. + - This is B(not considered during idempotency checks). + - This is only used by the C(pyopenssl) backend, or when I(encryption_level=compatibility2022). + - When using it, the default is C(2048) for C(pyopenssl) and C(50000) for C(cryptography). + type: int + maciter_size: + description: + - Number of times to repeat the MAC step. + - This is B(not considered during idempotency checks). + - This is only used by the C(pyopenssl) backend. When using it, the default is C(1). + type: int + encryption_level: + description: + - Determines the encryption level used. + - C(auto) uses the default of the selected backend. For C(cryptography), this is what the + cryptography library's specific version considers the best available encryption. + - C(compatibility2022) uses compatibility settings for older software in 2022. + This is only supported by the C(cryptography) backend if cryptography >= 38.0.0 is available. + - B(Note) that this option is B(not used for idempotency). + choices: + - auto + - compatibility2022 + default: auto + type: str + version_added: 2.8.0 + passphrase: + description: + - The PKCS#12 password. + - "B(Note:) PKCS12 encryption is not secure and should not be used as a security mechanism. + If you need to store or send a PKCS12 file safely, you should additionally encrypt it + with something else." + 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. + - Mutually exclusive with I(privatekey_content). + type: path + privatekey_content: + description: + - Content of the private key file. + - Mutually exclusive with I(privatekey_path). + type: str + version_added: "2.3.0" + 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: false + return_content: + description: + - If set to C(true), will return the (current or generated) PKCS#12's content as I(pkcs12). + type: bool + default: false + version_added: "1.0.0" + 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 I(iter_size) is used together with I(encryption_level != compatibility2022), or if I(maciter_size) is used, + C(auto) will always result in C(pyopenssl) to be chosen for backwards compatibility. + - 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 community.crypto x.y.0, and will be + # removed in community.crypto (x+1).0.0. + # From that point on, only the C(cryptography) backend will be available. + type: str + default: auto + choices: [ auto, cryptography, pyopenssl ] + version_added: 1.7.0 +seealso: + - module: community.crypto.x509_certificate + - module: community.crypto.openssl_csr + - module: community.crypto.openssl_dhparam + - module: community.crypto.openssl_privatekey + - module: community.crypto.openssl_publickey +''' + +EXAMPLES = r''' +- name: Generate PKCS#12 file + community.crypto.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 + # Note that if /opt/certs/ca.pem contains multiple certificates, + # only the first one will be used. See the other_certificates_parse_all + # option for changing this behavior. + state: present + +- name: Generate PKCS#12 file + community.crypto.openssl_pkcs12: + action: export + path: /opt/certs/ansible.p12 + friendly_name: raclette + privatekey_content: '{{ private_key_contents }}' + certificate_path: /opt/certs/cert.pem + other_certificates_parse_all: true + other_certificates: + - /opt/certs/ca_bundle.pem + # Since we set other_certificates_parse_all to true, all + # certificates in the CA bundle are included and not just + # the first one. + - /opt/certs/intermediate.pem + # In case this file has multiple certificates in it, + # all will be included as well. + state: present + +- name: Change PKCS#12 file permission + community.crypto.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 + community.crypto.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: true + +- name: Dump/Parse PKCS#12 file + community.crypto.openssl_pkcs12: + action: parse + src: /opt/certs/ansible.p12 + path: /opt/certs/ansible.pem + state: present + +- name: Remove PKCS#12 file + community.crypto.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(true) + 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(true) + type: str + version_added: "1.0.0" +''' + +import abc +import base64 +import os +import stat +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.text.converters import to_bytes, to_native + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + parse_pkcs12, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_privatekey, + load_certificate, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + split_pem_list, +) + +MINIMAL_CRYPTOGRAPHY_VERSION = '3.0' +MINIMAL_PYOPENSSL_VERSION = '0.15' + +PYOPENSSL_IMP_ERR = None +try: + import OpenSSL + from OpenSSL import crypto + PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) +except (ImportError, AttributeError): + 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 + from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + +CRYPTOGRAPHY_COMPATIBILITY2022_ERR = None +try: + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.serialization.pkcs12 import PBES + # Try to build encryption builder for compatibility2022 + serialization.PrivateFormat.PKCS12.encryption_builder().key_cert_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC).hmac_hash(hashes.SHA1()) +except Exception: + CRYPTOGRAPHY_COMPATIBILITY2022_ERR = traceback.format_exc() + CRYPTOGRAPHY_HAS_COMPATIBILITY2022 = False +else: + CRYPTOGRAPHY_HAS_COMPATIBILITY2022 = True + + +def load_certificate_set(filename, backend): + ''' + Load list of concatenated PEM files, and return a list of parsed certificates. + ''' + with open(filename, 'rb') as f: + data = f.read().decode('utf-8') + return [load_certificate(None, content=cert.encode('utf-8'), backend=backend) for cert in split_pem_list(data)] + + +class PkcsError(OpenSSLObjectError): + pass + + +class Pkcs(OpenSSLObject): + def __init__(self, module, backend, iter_size_default=2048): + super(Pkcs, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode + ) + self.backend = backend + self.action = module.params['action'] + self.other_certificates = module.params['other_certificates'] + self.other_certificates_parse_all = module.params['other_certificates_parse_all'] + self.certificate_path = module.params['certificate_path'] + self.friendly_name = module.params['friendly_name'] + self.iter_size = module.params['iter_size'] or iter_size_default + self.maciter_size = module.params['maciter_size'] or 1 + self.encryption_level = module.params['encryption_level'] + self.passphrase = module.params['passphrase'] + self.pkcs12 = None + self.privatekey_passphrase = module.params['privatekey_passphrase'] + self.privatekey_path = module.params['privatekey_path'] + self.privatekey_content = module.params['privatekey_content'] + 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 + + if self.privatekey_path is not None: + try: + with open(self.privatekey_path, 'rb') as fh: + self.privatekey_content = fh.read() + except (IOError, OSError) as exc: + raise PkcsError(exc) + elif self.privatekey_content is not None: + self.privatekey_content = to_bytes(self.privatekey_content) + + if self.other_certificates: + if self.other_certificates_parse_all: + filenames = list(self.other_certificates) + self.other_certificates = [] + for other_cert_bundle in filenames: + self.other_certificates.extend(load_certificate_set(other_cert_bundle, self.backend)) + else: + self.other_certificates = [ + load_certificate(other_cert, backend=self.backend) for other_cert in self.other_certificates + ] + + @abc.abstractmethod + def generate_bytes(self, module): + """Generate PKCS#12 file archive.""" + pass + + @abc.abstractmethod + def parse_bytes(self, pkcs12_content): + pass + + @abc.abstractmethod + def _dump_privatekey(self, pkcs12): + pass + + @abc.abstractmethod + def _dump_certificate(self, pkcs12): + pass + + @abc.abstractmethod + def _dump_other_certificates(self, pkcs12): + pass + + @abc.abstractmethod + def _get_friendly_name(self, pkcs12): + pass + + 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: + load_privatekey(None, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend) + except OpenSSLObjectError: + 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_bytes(module) + self.src = self.path + try: + pkcs12_privatekey, pkcs12_certificate, pkcs12_other_certificates, pkcs12_friendly_name = self.parse() + except OpenSSLObjectError: + return False + if (pkcs12_privatekey is not None) and (self.privatekey_content is not None): + expected_pkey = self._dump_privatekey(self.pkcs12) + if pkcs12_privatekey != expected_pkey: + return False + elif bool(pkcs12_privatekey) != bool(self.privatekey_content): + return False + + if (pkcs12_certificate is not None) and (self.certificate_path is not None): + expected_cert = self._dump_certificate(self.pkcs12) + 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 = self._dump_other_certificates(self.pkcs12) + 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 + friendly_name = self._get_friendly_name(self.pkcs12) + if ((friendly_name is not None) and (pkcs12_friendly_name is not None)): + if friendly_name != pkcs12_friendly_name: + return False + elif bool(friendly_name) != bool(pkcs12_friendly_name): + return False + elif module.params['action'] == 'parse' and os.path.exists(self.src) and os.path.exists(self.path): + try: + pkey, cert, other_certs, friendly_name = self.parse() + except OpenSSLObjectError: + return False + expected_content = to_bytes( + ''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None]) + ) + dumped_content = load_file_if_exists(self.path, ignore_errors=True) + if expected_content != dumped_content: + 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 = 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 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() + return self.parse_bytes(pkcs12_content) + except IOError as exc: + raise PkcsError(exc) + + def generate(self): + pass + + def write(self, module, content, mode=None): + """Write the PKCS#12 file.""" + if self.backup: + self.backup_file = module.backup_local(self.path) + write_file(module, content, mode) + if self.return_content: + self.pkcs12_bytes = content + + +class PkcsPyOpenSSL(Pkcs): + def __init__(self, module): + super(PkcsPyOpenSSL, self).__init__(module, 'pyopenssl') + if self.encryption_level != 'auto': + module.fail_json(msg='The PyOpenSSL backend only supports encryption_level = auto') + + def generate_bytes(self, module): + """Generate PKCS#12 file archive.""" + self.pkcs12 = crypto.PKCS12() + + if self.other_certificates: + self.pkcs12.set_ca_certificates(self.other_certificates) + + if self.certificate_path: + self.pkcs12.set_certificate(load_certificate(self.certificate_path, backend=self.backend)) + + if self.friendly_name: + self.pkcs12.set_friendlyname(to_bytes(self.friendly_name)) + + if self.privatekey_content: + try: + self.pkcs12.set_privatekey( + load_privatekey(None, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend)) + except OpenSSLBadPassphraseError as exc: + raise PkcsError(exc) + + return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size) + + def parse_bytes(self, pkcs12_content): + try: + p12 = crypto.load_pkcs12(pkcs12_content, self.passphrase) + pkey = p12.get_privatekey() + if pkey is not None: + pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey) + crt = p12.get_certificate() + if crt is not None: + crt = crypto.dump_certificate(crypto.FILETYPE_PEM, crt) + 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 crypto.Error as exc: + raise PkcsError(exc) + + def _dump_privatekey(self, pkcs12): + pk = pkcs12.get_privatekey() + return crypto.dump_privatekey(crypto.FILETYPE_PEM, pk) if pk else None + + def _dump_certificate(self, pkcs12): + cert = pkcs12.get_certificate() + return crypto.dump_certificate(crypto.FILETYPE_PEM, cert) if cert else None + + def _dump_other_certificates(self, pkcs12): + if pkcs12.get_ca_certificates() is None: + return [] + return [ + crypto.dump_certificate(crypto.FILETYPE_PEM, other_cert) + for other_cert in pkcs12.get_ca_certificates() + ] + + def _get_friendly_name(self, pkcs12): + return pkcs12.get_friendlyname() + + +class PkcsCryptography(Pkcs): + def __init__(self, module): + super(PkcsCryptography, self).__init__(module, 'cryptography', iter_size_default=50000) + if self.encryption_level == 'compatibility2022' and not CRYPTOGRAPHY_HAS_COMPATIBILITY2022: + module.fail_json( + msg='The installed cryptography version does not support encryption_level = compatibility2022.' + ' You need cryptography >= 38.0.0 and support for SHA1', + exception=CRYPTOGRAPHY_COMPATIBILITY2022_ERR) + + def generate_bytes(self, module): + """Generate PKCS#12 file archive.""" + pkey = None + if self.privatekey_content: + try: + pkey = load_privatekey(None, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend) + except OpenSSLBadPassphraseError as exc: + raise PkcsError(exc) + + cert = None + if self.certificate_path: + cert = load_certificate(self.certificate_path, backend=self.backend) + + friendly_name = to_bytes(self.friendly_name) if self.friendly_name is not None else None + + # Store fake object which can be used to retrieve the components back + self.pkcs12 = (pkey, cert, self.other_certificates, friendly_name) + + if not self.passphrase: + encryption = serialization.NoEncryption() + elif self.encryption_level == 'compatibility2022': + encryption = ( + serialization.PrivateFormat.PKCS12.encryption_builder(). + kdf_rounds(self.iter_size). + key_cert_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC). + hmac_hash(hashes.SHA1()). + build(to_bytes(self.passphrase)) + ) + else: + encryption = serialization.BestAvailableEncryption(to_bytes(self.passphrase)) + + return serialize_key_and_certificates( + friendly_name, + pkey, + cert, + self.other_certificates, + encryption, + ) + + def parse_bytes(self, pkcs12_content): + try: + private_key, certificate, additional_certificates, friendly_name = parse_pkcs12( + pkcs12_content, self.passphrase) + + pkey = None + if private_key is not None: + pkey = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + crt = None + if certificate is not None: + crt = certificate.public_bytes(serialization.Encoding.PEM) + + other_certs = [] + if additional_certificates is not None: + other_certs = [ + other_cert.public_bytes(serialization.Encoding.PEM) + for other_cert in additional_certificates + ] + + return (pkey, crt, other_certs, friendly_name) + except ValueError as exc: + raise PkcsError(exc) + + # The following methods will get self.pkcs12 passed, which is computed as: + # + # self.pkcs12 = (pkey, cert, self.other_certificates, self.friendly_name) + + def _dump_privatekey(self, pkcs12): + return pkcs12[0].private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) if pkcs12[0] else None + + def _dump_certificate(self, pkcs12): + return pkcs12[1].public_bytes(serialization.Encoding.PEM) if pkcs12[1] else None + + def _dump_other_certificates(self, pkcs12): + return [other_cert.public_bytes(serialization.Encoding.PEM) for other_cert in pkcs12[2]] + + def _get_friendly_name(self, pkcs12): + return pkcs12[3] + + +def select_backend(module, 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) + + # If no restrictions are provided, first try cryptography, then pyOpenSSL + if ( + (module.params['iter_size'] is not None and module.params['encryption_level'] != 'compatibility2022') + or module.params['maciter_size'] is not None + ): + # If iter_size (for encryption_level != compatibility2022) or maciter_size is specified, use pyOpenSSL backend + backend = 'pyopenssl' + elif can_use_cryptography: + backend = 'cryptography' + elif can_use_pyopenssl: + backend = 'pyopenssl' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Cannot 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='x.0.0', collection_name='community.crypto') + return backend, PkcsPyOpenSSL(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) + return backend, PkcsCryptography(module) + else: + raise ValueError('Unsupported value for backend: {0}'.format(backend)) + + +def main(): + argument_spec = dict( + action=dict(type='str', default='export', choices=['export', 'parse']), + other_certificates=dict(type='list', elements='path', aliases=['ca_certificates']), + other_certificates_parse_all=dict(type='bool', default=False), + certificate_path=dict(type='path'), + force=dict(type='bool', default=False), + friendly_name=dict(type='str', aliases=['name']), + encryption_level=dict(type='str', choices=['auto', 'compatibility2022'], default='auto'), + iter_size=dict(type='int'), + maciter_size=dict(type='int'), + 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'), + privatekey_content=dict(type='str', no_log=True), + 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), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), + ) + + required_if = [ + ['action', 'parse', ['src']], + ] + + mutually_exclusive = [ + ['privatekey_path', 'privatekey_content'], + ] + + module = AnsibleModule( + add_file_common_args=True, + argument_spec=argument_spec, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + backend, pkcs12 = select_backend(module, module.params['select_crypto_backend']) + + 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: + 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_bytes(module) + pkcs12.write(module, pkcs12_content, 0o600) + changed = True + else: + pkey, cert, other_certs, friendly_name = pkcs12.parse() + dump_content = ''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None]) + pkcs12.write(module, to_bytes(dump_content)) + changed = True + + file_args = module.load_file_common_arguments(module.params) + if module.check_file_absent_if_check_mode(file_args['path']): + changed = True + elif 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 OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_privatekey.py b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey.py new file mode 100644 index 000000000..7b50caff7 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey.py @@ -0,0 +1,290 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_privatekey +short_description: Generate OpenSSL private keys +description: + - This module allows one to (re)generate OpenSSL private keys. + - The default mode for the private key file will be C(0600) if I(mode) is not explicitly set. +author: + - Yanis Guenane (@Spredzy) + - Felix Fontein (@felixfontein) +extends_documentation_fragment: + - ansible.builtin.files + - community.crypto.attributes + - community.crypto.attributes.files + - community.crypto.module_privatekey +attributes: + check_mode: + support: full + diff_mode: + support: full + safe_file_operations: + support: full +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 ] + force: + description: + - Should the key be regenerated even if it already exists. + type: bool + default: false + path: + description: + - Name of the file in which the generated TLS/SSL private key will be written. It will have C(0600) mode + if I(mode) is not explicitly set. + type: path + required: true + format: + version_added: '1.0.0' + format_mismatch: + version_added: '1.0.0' + 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: false + return_content: + description: + - If set to C(true), 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! + - Use Ansible's I(no_log) task option to avoid the output being shown. See also + U(https://docs.ansible.com/ansible/latest/reference_appendices/faq.html#how-do-i-keep-secret-data-in-my-playbook). + type: bool + default: false + version_added: '1.0.0' + regenerate: + version_added: '1.0.0' +seealso: + - module: community.crypto.openssl_privatekey_pipe + - module: community.crypto.openssl_privatekey_info +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL private key with the default values (4096 bits, RSA) + community.crypto.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 + community.crypto.openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + passphrase: ansible + cipher: auto + +- name: Generate an OpenSSL private key with a different size (2048 bits) + community.crypto.openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + size: 2048 + +- name: Force regenerate an OpenSSL private key if it already exists + community.crypto.openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + force: true + +- name: Generate an OpenSSL private key with a different algorithm (DSA) + community.crypto.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. + 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(true) + 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(true) + type: str + version_added: '1.0.0' +''' + +import os + +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey import ( + select_backend, + get_privatekey_argument_spec, +) + + +class PrivateKeyModule(OpenSSLObject): + + def __init__(self, module, module_backend): + super(PrivateKeyModule, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode, + ) + self.module_backend = module_backend + self.return_content = module.params['return_content'] + if self.force: + module_backend.regenerate = 'always' + + self.backup = module.params['backup'] + self.backup_file = None + + if module.params['mode'] is None: + module.params['mode'] = '0600' + + module_backend.set_existing(load_file_if_exists(self.path, module)) + + def generate(self, module): + """Generate a keypair.""" + + if self.module_backend.needs_regeneration(): + # Regenerate + if not self.check_mode: + if self.backup: + self.backup_file = module.backup_local(self.path) + self.module_backend.generate_private_key() + privatekey_data = self.module_backend.get_private_key_data() + if self.return_content: + self.privatekey_bytes = privatekey_data + write_file(module, privatekey_data, 0o600) + self.changed = True + elif self.module_backend.needs_conversion(): + # Convert + if not self.check_mode: + if self.backup: + self.backup_file = module.backup_local(self.path) + self.module_backend.convert_private_key() + privatekey_data = self.module_backend.get_private_key_data() + if self.return_content: + self.privatekey_bytes = privatekey_data + write_file(module, privatekey_data, 0o600) + self.changed = True + + file_args = module.load_file_common_arguments(module.params) + if module.check_file_absent_if_check_mode(file_args['path']): + self.changed = True + else: + self.changed = module.set_fs_attributes_if_different(file_args, self.changed) + + def remove(self, module): + self.module_backend.set_existing(None) + if self.backup and not self.check_mode: + self.backup_file = module.backup_local(self.path) + super(PrivateKeyModule, self).remove(module) + + def dump(self): + """Serialize the object into a dictionary.""" + + result = self.module_backend.dump(include_key=self.return_content) + result['filename'] = self.path + result['changed'] = self.changed + if self.backup_file: + result['backup_file'] = self.backup_file + + return result + + +def main(): + + argument_spec = get_privatekey_argument_spec() + argument_spec.argument_spec.update(dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + force=dict(type='bool', default=False), + path=dict(type='path', required=True), + backup=dict(type='bool', default=False), + return_content=dict(type='bool', default=False), + )) + module = argument_spec.create_ansible_module( + 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 + ) + + backend, module_backend = select_backend( + module=module, + backend=module.params['select_crypto_backend'], + ) + + try: + private_key = PrivateKeyModule(module, module_backend) + + if private_key.state == 'present': + private_key.generate(module) + else: + private_key.remove(module) + + result = private_key.dump() + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_convert.py b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_convert.py new file mode 100644 index 000000000..5aec5cbe8 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_convert.py @@ -0,0 +1,171 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_privatekey_convert +short_description: Convert OpenSSL private keys +version_added: 2.1.0 +description: + - This module allows one to convert OpenSSL private keys. + - The default mode for the private key file will be C(0600) if I(mode) is not explicitly set. +author: + - Felix Fontein (@felixfontein) +extends_documentation_fragment: + - ansible.builtin.files + - community.crypto.attributes + - community.crypto.attributes.files + - community.crypto.module_privatekey_convert +attributes: + check_mode: + support: full + diff_mode: + support: none + safe_file_operations: + support: full +options: + dest_path: + description: + - Name of the file in which the generated TLS/SSL private key will be written. It will have C(0600) mode + if I(mode) is not explicitly set. + type: path + required: true + 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: false +seealso: [] +''' + +EXAMPLES = r''' +- name: Convert private key to PKCS8 format with passphrase + community.crypto.openssl_privatekey_convert: + src_path: /etc/ssl/private/ansible.com.pem + dest_path: /etc/ssl/private/ansible.com.key + dest_passphrase: '{{ private_key_passphrase }}' + format: pkcs8 +''' + +RETURN = r''' +backup_file: + description: Name of backup file created. + returned: changed and if I(backup) is C(true) + type: str + sample: /path/to/privatekey.pem.2019-03-09@11:22~ +''' + +import os + +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_convert import ( + select_backend, + get_privatekey_argument_spec, +) + + +class PrivateKeyConvertModule(OpenSSLObject): + def __init__(self, module, module_backend): + super(PrivateKeyConvertModule, self).__init__( + module.params['dest_path'], + 'present', + False, + module.check_mode, + ) + self.module_backend = module_backend + + self.backup = module.params['backup'] + self.backup_file = None + + module.params['path'] = module.params['dest_path'] + if module.params['mode'] is None: + module.params['mode'] = '0600' + + module_backend.set_existing_destination(load_file_if_exists(self.path, module)) + + def generate(self, module): + """Do conversion.""" + + if self.module_backend.needs_conversion(): + # Convert + privatekey_data = self.module_backend.get_private_key_data() + if not self.check_mode: + if self.backup: + self.backup_file = module.backup_local(self.path) + write_file(module, privatekey_data, 0o600) + self.changed = True + + file_args = module.load_file_common_arguments(module.params) + if module.check_file_absent_if_check_mode(file_args['path']): + self.changed = True + else: + self.changed = module.set_fs_attributes_if_different(file_args, self.changed) + + def dump(self): + """Serialize the object into a dictionary.""" + + result = self.module_backend.dump() + result['changed'] = self.changed + if self.backup_file: + result['backup_file'] = self.backup_file + + return result + + +def main(): + + argument_spec = get_privatekey_argument_spec() + argument_spec.argument_spec.update(dict( + dest_path=dict(type='path', required=True), + backup=dict(type='bool', default=False), + )) + module = argument_spec.create_ansible_module( + supports_check_mode=True, + add_file_common_args=True, + ) + + base_dir = os.path.dirname(module.params['dest_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 + ) + + module_backend = select_backend(module=module) + + try: + private_key = PrivateKeyConvertModule(module, module_backend) + + private_key.generate(module) + + result = private_key.dump() + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_info.py b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_info.py new file mode 100644 index 000000000..7eaec2348 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_info.py @@ -0,0 +1,278 @@ +#!/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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_privatekey_info +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 cryptography python library to interact with OpenSSL. +requirements: + - cryptography >= 1.2.3 +author: + - Felix Fontein (@felixfontein) + - Yanis Guenane (@Spredzy) +extends_documentation_fragment: + - community.crypto.attributes + - community.crypto.attributes.info_module +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: '1.0.0' + 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(true) when you want private information about this key to + leave the remote machine. + - "B(WARNING:) you have to make sure that private key data is not accidentally logged!" + type: bool + default: false + check_consistency: + description: + - Whether to check consistency of the private key. + - In community.crypto < 2.0.0, consistency was always checked. + - Since community.crypto 2.0.0, the consistency check has been disabled by default to + avoid private key material to be transported around and computed with, and only do + so when requested explicitly. This can potentially prevent + L(side-channel attacks,https://en.wikipedia.org/wiki/Side-channel_attack). + type: bool + default: false + version_added: 2.0.0 + + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography ] + +seealso: + - module: community.crypto.openssl_privatekey + - module: community.crypto.openssl_privatekey_pipe + - ref: community.crypto.openssl_privatekey_info filter <ansible_collections.community.crypto.openssl_privatekey_info_filter> + # - plugin: community.crypto.openssl_privatekey_info + # plugin_type: filter + description: A filter variant of this module. +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL private key with the default values (4096 bits, RSA) + community.crypto.openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + +- name: Get information on generated key + community.crypto.openssl_privatekey_info: + path: /etc/ssl/private/ansible.com.pem + register: result + +- name: Dump information + ansible.builtin.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(true) and + C(false), to indicate that consistency could not be checked. + - In case the check returns C(false), the module will fail. + returned: when I(check_consistency=true) + 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 + contains: + size: + description: + - Bit size of modulus (RSA) or prime number (DSA). + type: int + returned: When C(type=RSA) or C(type=DSA) + modulus: + description: + - The RSA key's modulus. + type: int + returned: When C(type=RSA) + exponent: + description: + - The RSA key's public exponent. + type: int + returned: When C(type=RSA) + p: + description: + - The C(p) value for DSA. + - This is the prime modulus upon which arithmetic takes place. + type: int + returned: When C(type=DSA) + q: + description: + - The C(q) value for DSA. + - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the + multiplicative group of the prime field used. + type: int + returned: When C(type=DSA) + g: + description: + - The C(g) value for DSA. + - This is the element spanning the subgroup of the multiplicative group of the prime field used. + type: int + returned: When C(type=DSA) + curve: + description: + - The curve's name for ECC. + type: str + returned: When C(type=ECC) + exponent_size: + description: + - The maximum number of bits of a private key. This is basically the bit size of the subgroup used. + type: int + returned: When C(type=ECC) + x: + description: + - The C(x) coordinate for the public point on the elliptic curve. + type: int + returned: When C(type=ECC) + y: + description: + - For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. + - For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. + type: int + returned: When C(type=DSA) or C(type=ECC) +private_data: + description: + - Private key data. Depends on key type. + returned: success and when I(return_private_key_data) is set to C(true) + type: dict +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import ( + PrivateKeyConsistencyError, + PrivateKeyParseError, + select_backend, +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + path=dict(type='path'), + content=dict(type='str', no_log=True), + passphrase=dict(type='str', no_log=True), + return_private_key_data=dict(type='bool', default=False), + check_consistency=dict(type='bool', default=False), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']), + ), + required_one_of=( + ['path', 'content'], + ), + mutually_exclusive=( + ['path', 'content'], + ), + supports_check_mode=True, + ) + + result = dict( + can_load_key=False, + can_parse_key=False, + key_is_consistent=None, + ) + + if module.params['content'] is not None: + data = module.params['content'].encode('utf-8') + else: + try: + with open(module.params['path'], 'rb') as f: + data = f.read() + except (IOError, OSError) as e: + module.fail_json(msg='Error while reading private key file from disk: {0}'.format(e), **result) + + result['can_load_key'] = True + + backend, module_backend = select_backend( + module, + module.params['select_crypto_backend'], + data, + passphrase=module.params['passphrase'], + return_private_key_data=module.params['return_private_key_data'], + check_consistency=module.params['check_consistency']) + + try: + result.update(module_backend.get_info()) + module.exit_json(**result) + except PrivateKeyParseError as exc: + result.update(exc.result) + module.fail_json(msg=exc.error_message, **result) + except PrivateKeyConsistencyError as exc: + result.update(exc.result) + module.fail_json(msg=exc.error_message, **result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_pipe.py b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_pipe.py new file mode 100644 index 000000000..41432840d --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_pipe.py @@ -0,0 +1,132 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_privatekey_pipe +short_description: Generate OpenSSL private keys without disk access +version_added: 1.3.0 +description: + - This module allows one to (re)generate OpenSSL private keys without disk access. + - This allows to read and write keys to vaults without having to write intermediate versions to disk. + - Make sure to not write the result of this module into logs or to the console, as it contains private key data! Use the I(no_log) task option to be sure. + - Note that this module is implemented as an L(action plugin,https://docs.ansible.com/ansible/latest/plugins/action.html) + and will always be executed on the controller. +author: + - Yanis Guenane (@Spredzy) + - Felix Fontein (@felixfontein) +extends_documentation_fragment: + - community.crypto.attributes + - community.crypto.attributes.flow + - community.crypto.module_privatekey +attributes: + action: + support: full + async: + support: none + details: + - This action runs completely on the controller. + check_mode: + support: full + diff_mode: + support: full +options: + content: + description: + - The current private key data. + - Needed for idempotency. If not provided, the module will always return a change, and all idempotence-related + options are ignored. + type: str + content_base64: + description: + - Set to C(true) if the content is base64 encoded. + type: bool + default: false + return_current_key: + description: + - Set to C(true) to return the current private key when the module did not generate a new one. + - Note that in case of check mode, when this option is not set to C(true), the module always returns the + current key (if it was provided) and Ansible will replace it by C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER). + type: bool + default: false +seealso: + - module: community.crypto.openssl_privatekey + - module: community.crypto.openssl_privatekey_info +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL private key with the default values (4096 bits, RSA) + community.crypto.openssl_privatekey_pipe: + register: output + no_log: true # make sure that private key data is not accidentally revealed in logs! +- name: Show generated key + ansible.builtin.debug: + msg: "{{ output.privatekey }}" + # DO NOT OUTPUT KEY MATERIAL TO CONSOLE OR LOGS IN PRODUCTION! + + +- name: Generate or update a Mozilla sops encrypted key + block: + - name: Update sops-encrypted key with the community.sops collection + community.crypto.openssl_privatekey_pipe: + content: "{{ lookup('community.sops.sops', 'private_key.pem.sops') }}" + size: 2048 + register: output + no_log: true # make sure that private key data is not accidentally revealed in logs! + + - name: Update encrypted key when openssl_privatekey_pipe reported a change + community.sops.sops_encrypt: + path: private_key.pem.sops + content_text: "{{ output.privatekey }}" + when: output is changed + always: + - name: Make sure that output (which contains the private key) is overwritten + ansible.builtin.set_fact: + output: '' +''' + +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 +fingerprint: + description: + - The fingerprint of the public key. Fingerprint will be generated for each C(hashlib.algorithms) available. + 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" +privatekey: + description: + - The generated private key's content. + - Please note that if the result is not changed, the current private key will only be returned + if the I(return_current_key) option is set to C(true). + - Will be Base64-encoded if the key is in raw format. + returned: changed, or I(return_current_key) is C(true) + type: str +''' diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_publickey.py b/ansible_collections/community/crypto/plugins/modules/openssl_publickey.py new file mode 100644 index 000000000..da01d1fb4 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_publickey.py @@ -0,0 +1,488 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_publickey +short_description: Generate an OpenSSL public key from its private key. +description: + - This module allows one to (re)generate public keys from their private keys. + - Public keys are generated in PEM or OpenSSH format. Private keys must be OpenSSL PEM keys. + OpenSSH private keys are not supported, use the M(community.crypto.openssh_keypair) module to manage these. + - The module uses the cryptography Python library. +requirements: + - cryptography >= 1.2.3 (older versions might work as well) + - Needs cryptography >= 1.4 if I(format) is C(OpenSSH) +author: + - Yanis Guenane (@Spredzy) + - Felix Fontein (@felixfontein) +extends_documentation_fragment: + - ansible.builtin.files + - community.crypto.attributes + - community.crypto.attributes.files +attributes: + check_mode: + support: full + diff_mode: + support: full + safe_file_operations: + support: full +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: false + format: + description: + - The format of the public key. + type: str + default: PEM + choices: [ OpenSSH, PEM ] + 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: '1.0.0' + privatekey_passphrase: + description: + - The passphrase for the private key. + type: str + 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: false + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography ] + return_content: + description: + - If set to C(true), will return the (current or generated) public key's content as I(publickey). + type: bool + default: false + version_added: '1.0.0' +seealso: + - module: community.crypto.x509_certificate + - module: community.crypto.x509_certificate_pipe + - module: community.crypto.openssl_csr + - module: community.crypto.openssl_csr_pipe + - module: community.crypto.openssl_dhparam + - module: community.crypto.openssl_pkcs12 + - module: community.crypto.openssl_privatekey + - module: community.crypto.openssl_privatekey_pipe +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL public key in PEM format + community.crypto.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 + community.crypto.openssl_publickey: + path: /etc/ssl/public/ansible.com.pem + privatekey_content: "{{ private_key_content }}" + +- name: Generate an OpenSSL public key in OpenSSH v2 format + community.crypto.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 + community.crypto.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 + community.crypto.openssl_publickey: + path: /etc/ssl/public/ansible.com.pem + privatekey_path: /etc/ssl/private/ansible.com.pem + force: true + +- name: Remove an OpenSSL public key + community.crypto.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. + 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(true) + 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(true) + type: str + version_added: '1.0.0' +''' + +import os +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_privatekey, + get_fingerprint, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import ( + PublicKeyParseError, + get_publickey_info, +) + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' +MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH = '1.4' + +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 + + +class PublicKeyError(OpenSSLObjectError): + pass + + +class PublicKey(OpenSSLObject): + + def __init__(self, module, backend): + super(PublicKey, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode + ) + self.module = module + 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 + + self.diff_before = self._get_info(None) + self.diff_after = self._get_info(None) + + def _get_info(self, data): + if data is None: + return dict() + result = dict(can_parse_key=False) + try: + result.update(get_publickey_info( + self.module, self.backend, content=data, prefer_one_fingerprint=True)) + result['can_parse_key'] = True + except PublicKeyParseError as exc: + result.update(exc.result) + except Exception as exc: + pass + return result + + def _create_publickey(self, module): + self.privatekey = 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 + ) + + 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) + self.diff_after = self._get_info(publickey_content) + if self.return_content: + self.publickey_bytes = publickey_content + + if self.backup: + self.backup_file = module.backup_local(self.path) + write_file(module, publickey_content) + + self.changed = True + except OpenSSLBadPassphraseError as exc: + raise PublicKeyError(exc) + except (IOError, OSError) as exc: + raise PublicKeyError(exc) + + self.fingerprint = 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.check_file_absent_if_check_mode(file_args['path']): + self.changed = True + elif 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() + self.diff_before = self.diff_after = self._get_info(publickey_content) + 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 + ) + except Exception as dummy: + return False + + try: + desired_publickey = self._create_publickey(module) + except 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 = load_file_if_exists(self.path, ignore_errors=True) + result['publickey'] = self.publickey_bytes.decode('utf-8') if self.publickey_bytes else None + + result['diff'] = dict( + before=self.diff_before, + after=self.diff_after, + ) + + 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', no_log=True), + 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', '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) + + # Decision + if can_use_cryptography: + backend = 'cryptography' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Cannot detect the required Python library " + "cryptography (>= {0})").format(minimal_cryptography_version)) + + if module.params['format'] == 'OpenSSH' and backend != 'cryptography': + module.fail_json(msg="Format OpenSSH requires the cryptography backend.") + + if 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 OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_publickey_info.py b/ansible_collections/community/crypto/plugins/modules/openssl_publickey_info.py new file mode 100644 index 000000000..7b0610065 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_publickey_info.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_publickey_info +short_description: Provide information for OpenSSL public keys +description: + - This module allows one to query information on OpenSSL public keys. + - It uses the cryptography python library to interact with OpenSSL. +version_added: 1.7.0 +requirements: + - cryptography >= 1.2.3 +author: + - Felix Fontein (@felixfontein) +extends_documentation_fragment: + - community.crypto.attributes + - community.crypto.attributes.info_module +options: + path: + description: + - Remote absolute path where the public key file is loaded from. + type: path + content: + description: + - Content of the public key file. + - Either I(path) or I(content) must be specified, but not both. + type: str + + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography ] + +seealso: + - module: community.crypto.openssl_publickey + - module: community.crypto.openssl_privatekey_info + - ref: community.crypto.openssl_publickey_info filter <ansible_collections.community.crypto.openssl_publickey_info_filter> + # - plugin: community.crypto.openssl_publickey_info + # plugin_type: filter + description: A filter variant of this module. +''' + +EXAMPLES = r''' +- name: Generate an OpenSSL private key with the default values (4096 bits, RSA) + community.crypto.openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + +- name: Create public key from private key + community.crypto.openssl_publickey: + privatekey_path: /etc/ssl/private/ansible.com.pem + path: /etc/ssl/ansible.com.pub + +- name: Get information on public key + community.crypto.openssl_publickey_info: + path: /etc/ssl/ansible.com.pub + register: result + +- name: Dump information + ansible.builtin.debug: + var: result +''' + +RETURN = r''' +fingerprints: + description: + - Fingerprints of 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 + contains: + size: + description: + - Bit size of modulus (RSA) or prime number (DSA). + type: int + returned: When C(type=RSA) or C(type=DSA) + modulus: + description: + - The RSA key's modulus. + type: int + returned: When C(type=RSA) + exponent: + description: + - The RSA key's public exponent. + type: int + returned: When C(type=RSA) + p: + description: + - The C(p) value for DSA. + - This is the prime modulus upon which arithmetic takes place. + type: int + returned: When C(type=DSA) + q: + description: + - The C(q) value for DSA. + - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the + multiplicative group of the prime field used. + type: int + returned: When C(type=DSA) + g: + description: + - The C(g) value for DSA. + - This is the element spanning the subgroup of the multiplicative group of the prime field used. + type: int + returned: When C(type=DSA) + curve: + description: + - The curve's name for ECC. + type: str + returned: When C(type=ECC) + exponent_size: + description: + - The maximum number of bits of a private key. This is basically the bit size of the subgroup used. + type: int + returned: When C(type=ECC) + x: + description: + - The C(x) coordinate for the public point on the elliptic curve. + type: int + returned: When C(type=ECC) + y: + description: + - For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. + - For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. + type: int + returned: When C(type=DSA) or C(type=ECC) +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import ( + PublicKeyParseError, + select_backend, +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + path=dict(type='path'), + content=dict(type='str', no_log=True), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']), + ), + required_one_of=( + ['path', 'content'], + ), + mutually_exclusive=( + ['path', 'content'], + ), + supports_check_mode=True, + ) + + result = dict( + can_load_key=False, + can_parse_key=False, + key_is_consistent=None, + ) + + if module.params['content'] is not None: + data = module.params['content'].encode('utf-8') + else: + try: + with open(module.params['path'], 'rb') as f: + data = f.read() + except (IOError, OSError) as e: + module.fail_json(msg='Error while reading public key file from disk: {0}'.format(e), **result) + + backend, module_backend = select_backend( + module, + module.params['select_crypto_backend'], + data) + + try: + result.update(module_backend.get_info()) + module.exit_json(**result) + except PublicKeyParseError as exc: + result.update(exc.result) + module.fail_json(msg=exc.error_message, **result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_signature.py b/ansible_collections/community/crypto/plugins/modules/openssl_signature.py new file mode 100644 index 000000000..43503bd1d --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_signature.py @@ -0,0 +1,276 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2019, Patrick Pichler <ppichler+ansible@mgit.at> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_signature +version_added: 1.1.0 +short_description: Sign data with openssl +description: + - This module allows one to sign data using a private key. + - The module uses the cryptography Python library. +requirements: + - cryptography >= 1.4 (some key types require newer versions) +author: + - Patrick Pichler (@aveexy) + - Markus Teufelberger (@MarkusTeufelberger) +extends_documentation_fragment: + - community.crypto.attributes +attributes: + check_mode: + support: full + details: + - This action does not modify state. + diff_mode: + support: none +options: + privatekey_path: + description: + - The path to the private key to use when signing. + - Either I(privatekey_path) or I(privatekey_content) must be specified, but not both. + type: path + privatekey_content: + description: + - The content of the private key to use when signing the certificate signing request. + - Either I(privatekey_path) or I(privatekey_content) must be specified, but not both. + type: str + privatekey_passphrase: + description: + - The passphrase for the private key. + - This is required if the private key is password protected. + type: str + path: + description: + - The file to sign. + - This file will only be read and not modified. + type: path + required: true + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography ] +notes: + - | + When using the C(cryptography) backend, the following key types require at least the following C(cryptography) version: + RSA keys: C(cryptography) >= 1.4 + DSA and ECDSA keys: C(cryptography) >= 1.5 + ed448 and ed25519 keys: C(cryptography) >= 2.6 +seealso: + - module: community.crypto.openssl_signature_info + - module: community.crypto.openssl_privatekey +''' + +EXAMPLES = r''' +- name: Sign example file + community.crypto.openssl_signature: + privatekey_path: private.key + path: /tmp/example_file + register: sig + +- name: Verify signature of example file + community.crypto.openssl_signature_info: + certificate_path: cert.pem + path: /tmp/example_file + signature: "{{ sig.signature }}" + register: verify + +- name: Make sure the signature is valid + ansible.builtin.assert: + that: + - verify.valid +''' + +RETURN = r''' +signature: + description: Base64 encoded signature. + returned: success + type: str +''' + +import os +import traceback +import base64 + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.4' + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + import cryptography.hazmat.primitives.asymmetric.padding + import cryptography.hazmat.primitives.hashes + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + CRYPTOGRAPHY_HAS_DSA_SIGN, + CRYPTOGRAPHY_HAS_EC_SIGN, + CRYPTOGRAPHY_HAS_ED25519_SIGN, + CRYPTOGRAPHY_HAS_ED448_SIGN, + CRYPTOGRAPHY_HAS_RSA_SIGN, + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_privatekey, +) + +from ansible.module_utils.common.text.converters import to_native +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + + +class SignatureBase(OpenSSLObject): + + def __init__(self, module, backend): + super(SignatureBase, self).__init__( + path=module.params['path'], + state='present', + force=False, + check_mode=module.check_mode + ) + + self.backend = backend + + 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'] + + def generate(self): + # Empty method because OpenSSLObject wants this + pass + + def dump(self): + # Empty method because OpenSSLObject wants this + pass + + +# Implementation with using cryptography +class SignatureCryptography(SignatureBase): + + def __init__(self, module, backend): + super(SignatureCryptography, self).__init__(module, backend) + + def run(self): + _padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15() + _hash = cryptography.hazmat.primitives.hashes.SHA256() + + result = dict() + + try: + with open(self.path, "rb") as f: + _in = f.read() + + private_key = load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend=self.backend, + ) + + signature = None + + if CRYPTOGRAPHY_HAS_DSA_SIGN: + if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): + signature = private_key.sign(_in, _hash) + + if CRYPTOGRAPHY_HAS_EC_SIGN: + if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): + signature = private_key.sign(_in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash)) + + if CRYPTOGRAPHY_HAS_ED25519_SIGN: + if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): + signature = private_key.sign(_in) + + if CRYPTOGRAPHY_HAS_ED448_SIGN: + if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): + signature = private_key.sign(_in) + + if CRYPTOGRAPHY_HAS_RSA_SIGN: + if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + signature = private_key.sign(_in, _padding, _hash) + + if signature is None: + self.module.fail_json( + msg="Unsupported key type. Your cryptography version is {0}".format(CRYPTOGRAPHY_VERSION) + ) + + result['signature'] = base64.b64encode(signature) + return result + + except Exception as e: + raise OpenSSLObjectError(e) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + privatekey_path=dict(type='path'), + privatekey_content=dict(type='str', no_log=True), + privatekey_passphrase=dict(type='str', no_log=True), + path=dict(type='path', required=True), + select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'), + ), + mutually_exclusive=( + ['privatekey_path', 'privatekey_content'], + ), + required_one_of=( + ['privatekey_path', 'privatekey_content'], + ), + supports_check_mode=True, + ) + + if not os.path.isfile(module.params['path']): + module.fail_json( + name=module.params['path'], + msg='The file {0} does not exist'.format(module.params['path']) + ) + + 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) + + # Decision + if can_use_cryptography: + backend = 'cryptography' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Cannot detect the required Python library " + "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) + try: + if backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + _sign = SignatureCryptography(module, backend) + + result = _sign.run() + + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_signature_info.py b/ansible_collections/community/crypto/plugins/modules/openssl_signature_info.py new file mode 100644 index 000000000..b83f3e693 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/openssl_signature_info.py @@ -0,0 +1,299 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2019, Patrick Pichler <ppichler+ansible@mgit.at> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_signature_info +version_added: 1.1.0 +short_description: Verify signatures with openssl +description: + - This module allows one to verify a signature for a file by a certificate. + - The module uses the cryptography Python library. +requirements: + - cryptography >= 1.4 (some key types require newer versions) +author: + - Patrick Pichler (@aveexy) + - Markus Teufelberger (@MarkusTeufelberger) +extends_documentation_fragment: + - community.crypto.attributes + - community.crypto.attributes.info_module +options: + path: + description: + - The signed file to verify. + - This file will only be read and not modified. + type: path + required: true + certificate_path: + description: + - The path to the certificate used to verify the signature. + - Either I(certificate_path) or I(certificate_content) must be specified, but not both. + type: path + certificate_content: + description: + - The content of the certificate used to verify the signature. + - Either I(certificate_path) or I(certificate_content) must be specified, but not both. + type: str + signature: + description: Base64 encoded signature. + type: str + required: true + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography ] +notes: + - | + When using the C(cryptography) backend, the following key types require at least the following C(cryptography) version: + RSA keys: C(cryptography) >= 1.4 + DSA and ECDSA keys: C(cryptography) >= 1.5 + ed448 and ed25519 keys: C(cryptography) >= 2.6 +seealso: + - module: community.crypto.openssl_signature + - module: community.crypto.x509_certificate +''' + +EXAMPLES = r''' +- name: Sign example file + community.crypto.openssl_signature: + privatekey_path: private.key + path: /tmp/example_file + register: sig + +- name: Verify signature of example file + community.crypto.openssl_signature_info: + certificate_path: cert.pem + path: /tmp/example_file + signature: "{{ sig.signature }}" + register: verify + +- name: Make sure the signature is valid + ansible.builtin.assert: + that: + - verify.valid +''' + +RETURN = r''' +valid: + description: C(true) means the signature was valid for the given file, C(false) means it was not. + returned: success + type: bool +''' + +import os +import traceback +import base64 + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.4' + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + import cryptography.hazmat.primitives.asymmetric.padding + import cryptography.hazmat.primitives.hashes + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + CRYPTOGRAPHY_HAS_DSA_SIGN, + CRYPTOGRAPHY_HAS_EC_SIGN, + CRYPTOGRAPHY_HAS_ED25519_SIGN, + CRYPTOGRAPHY_HAS_ED448_SIGN, + CRYPTOGRAPHY_HAS_RSA_SIGN, + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_certificate, +) + +from ansible.module_utils.common.text.converters import to_native +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + + +class SignatureInfoBase(OpenSSLObject): + + def __init__(self, module, backend): + super(SignatureInfoBase, self).__init__( + path=module.params['path'], + state='present', + force=False, + check_mode=module.check_mode + ) + + self.backend = backend + + self.signature = module.params['signature'] + self.certificate_path = module.params['certificate_path'] + self.certificate_content = module.params['certificate_content'] + if self.certificate_content is not None: + self.certificate_content = self.certificate_content.encode('utf-8') + + def generate(self): + # Empty method because OpenSSLObject wants this + pass + + def dump(self): + # Empty method because OpenSSLObject wants this + pass + + +# Implementation with using cryptography +class SignatureInfoCryptography(SignatureInfoBase): + + def __init__(self, module, backend): + super(SignatureInfoCryptography, self).__init__(module, backend) + + def run(self): + _padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15() + _hash = cryptography.hazmat.primitives.hashes.SHA256() + + result = dict() + + try: + with open(self.path, "rb") as f: + _in = f.read() + + _signature = base64.b64decode(self.signature) + certificate = load_certificate( + path=self.certificate_path, + content=self.certificate_content, + backend=self.backend, + ) + public_key = certificate.public_key() + verified = False + valid = False + + if CRYPTOGRAPHY_HAS_DSA_SIGN: + try: + if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey): + public_key.verify(_signature, _in, _hash) + verified = True + valid = True + except cryptography.exceptions.InvalidSignature: + verified = True + valid = False + + if CRYPTOGRAPHY_HAS_EC_SIGN: + try: + if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey): + public_key.verify(_signature, _in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash)) + verified = True + valid = True + except cryptography.exceptions.InvalidSignature: + verified = True + valid = False + + if CRYPTOGRAPHY_HAS_ED25519_SIGN: + try: + if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey): + public_key.verify(_signature, _in) + verified = True + valid = True + except cryptography.exceptions.InvalidSignature: + verified = True + valid = False + + if CRYPTOGRAPHY_HAS_ED448_SIGN: + try: + if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey): + public_key.verify(_signature, _in) + verified = True + valid = True + except cryptography.exceptions.InvalidSignature: + verified = True + valid = False + + if CRYPTOGRAPHY_HAS_RSA_SIGN: + try: + if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey): + public_key.verify(_signature, _in, _padding, _hash) + verified = True + valid = True + except cryptography.exceptions.InvalidSignature: + verified = True + valid = False + + if not verified: + self.module.fail_json( + msg="Unsupported key type. Your cryptography version is {0}".format(CRYPTOGRAPHY_VERSION) + ) + result['valid'] = valid + return result + + except Exception as e: + raise OpenSSLObjectError(e) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + certificate_path=dict(type='path'), + certificate_content=dict(type='str'), + path=dict(type='path', required=True), + signature=dict(type='str', required=True), + select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'), + ), + mutually_exclusive=( + ['certificate_path', 'certificate_content'], + ), + required_one_of=( + ['certificate_path', 'certificate_content'], + ), + supports_check_mode=True, + ) + + if not os.path.isfile(module.params['path']): + module.fail_json( + name=module.params['path'], + msg='The file {0} does not exist'.format(module.params['path']) + ) + + 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) + + # Decision + if can_use_cryptography: + backend = 'cryptography' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Cannot detect any of the required Python libraries " + "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) + try: + if backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + _sign = SignatureInfoCryptography(module, backend) + + result = _sign.run() + + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/plugins/modules/x509_certificate.py b/ansible_collections/community/crypto/plugins/modules/x509_certificate.py new file mode 100644 index 000000000..1b4ece5cb --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/x509_certificate.py @@ -0,0 +1,420 @@ +#!/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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: x509_certificate +short_description: Generate and/or check OpenSSL certificates +description: + - It implements a notion of provider (one of C(selfsigned), C(ownca), C(acme), and C(entrust)) + for your certificate. + - "Please note that the module regenerates existing certificate if it does not match the module's + options, or if it seems to be corrupt. If you are concerned that this could overwrite + your existing certificate, consider using the I(backup) option." + - Note that this module was called C(openssl_certificate) when included directly in Ansible up to version 2.9. + When moved to the collection C(community.crypto), it was renamed to + M(community.crypto.x509_certificate). From Ansible 2.10 on, it can still be used by the + old short name (or by C(ansible.builtin.openssl_certificate)), which redirects to + C(community.crypto.x509_certificate). When using FQCNs or when using the + L(collections,https://docs.ansible.com/ansible/latest/user_guide/collections_using.html#using-collections-in-a-playbook) + keyword, the new name M(community.crypto.x509_certificate) should be used to avoid + a deprecation warning. +author: + - Yanis Guenane (@Spredzy) + - Markus Teufelberger (@MarkusTeufelberger) +extends_documentation_fragment: + - ansible.builtin.files + - community.crypto.attributes + - community.crypto.attributes.files + - community.crypto.module_certificate + - community.crypto.module_certificate.backend_acme_documentation + - community.crypto.module_certificate.backend_entrust_documentation + - community.crypto.module_certificate.backend_ownca_documentation + - community.crypto.module_certificate.backend_selfsigned_documentation +attributes: + check_mode: + support: full + diff_mode: + support: full + safe_file_operations: + support: full +options: + state: + description: + - Whether the certificate should exist or not, taking action if the state is different from what is stated. + type: str + default: present + choices: [ absent, present ] + + path: + description: + - Remote absolute path where the generated certificate file should be created or is already located. + type: path + required: true + + provider: + description: + - Name of the provider to use to generate/retrieve the OpenSSL certificate. + Please see the examples on how to emulate it with + M(community.crypto.x509_certificate_info), M(community.crypto.openssl_csr_info), + M(community.crypto.openssl_privatekey_info) and M(ansible.builtin.assert). + - "The C(entrust) provider was added for Ansible 2.9 and requires credentials for the + L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API." + - Required if I(state) is C(present). + type: str + choices: [ acme, entrust, ownca, selfsigned ] + + return_content: + description: + - If set to C(true), will return the (current or generated) certificate's content as I(certificate). + type: bool + default: false + version_added: '1.0.0' + + backup: + description: + - Create a backup file including a timestamp so you can get the original + certificate back if you overwrote it with a new one by accident. + type: bool + default: false + + csr_content: + version_added: '1.0.0' + privatekey_content: + version_added: '1.0.0' + acme_directory: + version_added: '1.0.0' + ownca_content: + version_added: '1.0.0' + ownca_privatekey_content: + version_added: '1.0.0' + +seealso: + - module: community.crypto.x509_certificate_pipe +''' + +EXAMPLES = r''' +- name: Generate a Self Signed OpenSSL certificate + community.crypto.x509_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 + +- name: Generate an OpenSSL certificate signed with your own CA certificate + community.crypto.x509_certificate: + path: /etc/ssl/crt/ansible.com.crt + csr_path: /etc/ssl/csr/ansible.com.csr + ownca_path: /etc/ssl/crt/ansible_CA.crt + ownca_privatekey_path: /etc/ssl/private/ansible_CA.pem + provider: ownca + +- name: Generate a Let's Encrypt Certificate + community.crypto.x509_certificate: + path: /etc/ssl/crt/ansible.com.crt + csr_path: /etc/ssl/csr/ansible.com.csr + provider: acme + acme_accountkey_path: /etc/ssl/private/ansible.com.pem + acme_challenge_path: /etc/ssl/challenges/ansible.com/ + +- name: Force (re-)generate a new Let's Encrypt Certificate + community.crypto.x509_certificate: + path: /etc/ssl/crt/ansible.com.crt + csr_path: /etc/ssl/csr/ansible.com.csr + provider: acme + acme_accountkey_path: /etc/ssl/private/ansible.com.pem + acme_challenge_path: /etc/ssl/challenges/ansible.com/ + force: true + +- name: Generate an Entrust certificate via the Entrust Certificate Services (ECS) API + community.crypto.x509_certificate: + path: /etc/ssl/crt/ansible.com.crt + csr_path: /etc/ssl/csr/ansible.com.csr + provider: entrust + entrust_requester_name: Jo Doe + entrust_requester_email: jdoe@ansible.com + entrust_requester_phone: 555-555-5555 + entrust_cert_type: STANDARD_SSL + entrust_api_user: apiusername + entrust_api_key: a^lv*32!cd9LnT + entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt + entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-key.crt + entrust_api_specification_path: /etc/ssl/entrust/api-docs/cms-api-2.1.0.yaml + +# The following example shows how to emulate the behavior of the removed +# "assertonly" provider with the x509_certificate_info, openssl_csr_info, +# openssl_privatekey_info and assert modules: + +- name: Get certificate information + community.crypto.x509_certificate_info: + path: /etc/ssl/crt/ansible.com.crt + # for valid_at, invalid_at and valid_in + valid_at: + one_day_ten_hours: "+1d10h" + fixed_timestamp: 20200331202428Z + ten_seconds: "+10" + register: result + +- name: Get CSR information + community.crypto.openssl_csr_info: + # Verifies that the CSR signature is valid; module will fail if not + path: /etc/ssl/csr/ansible.com.csr + register: result_csr + +- name: Get private key information + community.crypto.openssl_privatekey_info: + path: /etc/ssl/csr/ansible.com.key + register: result_privatekey + +- name: Check conditions on certificate, CSR, and private key + ansible.builtin.assert: + that: + # When private key was specified for assertonly, this was checked: + - result.public_key == result_privatekey.public_key + # When CSR was specified for assertonly, this was checked: + - result.public_key == result_csr.public_key + - result.subject_ordered == result_csr.subject_ordered + - result.extensions_by_oid == result_csr.extensions_by_oid + # signature_algorithms check + - "result.signature_algorithm == 'sha256WithRSAEncryption' or result.signature_algorithm == 'sha512WithRSAEncryption'" + # subject and subject_strict + - "result.subject.commonName == 'ansible.com'" + - "result.subject | length == 1" # the number must be the number of entries you check for + # issuer and issuer_strict + - "result.issuer.commonName == 'ansible.com'" + - "result.issuer | length == 1" # the number must be the number of entries you check for + # has_expired + - not result.expired + # version + - result.version == 3 + # key_usage and key_usage_strict + - "'Data Encipherment' in result.key_usage" + - "result.key_usage | length == 1" # the number must be the number of entries you check for + # extended_key_usage and extended_key_usage_strict + - "'DVCS' in result.extended_key_usage" + - "result.extended_key_usage | length == 1" # the number must be the number of entries you check for + # subject_alt_name and subject_alt_name_strict + - "'dns:ansible.com' in result.subject_alt_name" + - "result.subject_alt_name | length == 1" # the number must be the number of entries you check for + # not_before and not_after + - "result.not_before == '20190331202428Z'" + - "result.not_after == '20190413202428Z'" + # valid_at, invalid_at and valid_in + - "result.valid_at.one_day_ten_hours" # for valid_at + - "not result.valid_at.fixed_timestamp" # for invalid_at + - "result.valid_at.ten_seconds" # for valid_in +''' + +RETURN = r''' +filename: + description: Path to 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. + returned: changed and if I(backup) is C(true) + type: str + sample: /path/to/www.ansible.com.crt.2019-03-09@11:22~ +certificate: + description: The (current or generated) certificate's content. + returned: if I(state) is C(present) and I(return_content) is C(true) + type: str + version_added: '1.0.0' +''' + + +import os + +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( + select_backend, + get_certificate_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_acme import ( + AcmeCertificateProvider, + add_acme_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_entrust import ( + EntrustCertificateProvider, + add_entrust_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_ownca import ( + OwnCACertificateProvider, + add_ownca_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_selfsigned import ( + SelfSignedCertificateProvider, + add_selfsigned_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, +) + + +class CertificateAbsent(OpenSSLObject): + def __init__(self, module): + super(CertificateAbsent, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode + ) + self.module = module + self.return_content = module.params['return_content'] + self.backup = module.params['backup'] + self.backup_file = None + + def generate(self, module): + pass + + def remove(self, module): + if self.backup: + self.backup_file = module.backup_local(self.path) + super(CertificateAbsent, self).remove(module) + + def dump(self, check_mode=False): + result = { + 'changed': self.changed, + 'filename': self.path, + 'privatekey': self.module.params['privatekey_path'], + 'csr': self.module.params['csr_path'] + } + if self.backup_file: + result['backup_file'] = self.backup_file + if self.return_content: + result['certificate'] = None + + return result + + +class GenericCertificate(OpenSSLObject): + """Retrieve a certificate using the given module backend.""" + def __init__(self, module, module_backend): + super(GenericCertificate, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode + ) + self.module = module + self.return_content = module.params['return_content'] + self.backup = module.params['backup'] + self.backup_file = None + + self.module_backend = module_backend + self.module_backend.set_existing(load_file_if_exists(self.path, module)) + + def generate(self, module): + if self.module_backend.needs_regeneration(): + if not self.check_mode: + self.module_backend.generate_certificate() + result = self.module_backend.get_certificate_data() + if self.backup: + self.backup_file = module.backup_local(self.path) + write_file(module, result) + self.changed = True + + file_args = module.load_file_common_arguments(module.params) + if module.check_file_absent_if_check_mode(file_args['path']): + self.changed = True + else: + self.changed = module.set_fs_attributes_if_different(file_args, self.changed) + + def check(self, module, perms_required=True): + """Ensure the resource is in its desired state.""" + return super(GenericCertificate, self).check(module, perms_required) and not self.module_backend.needs_regeneration() + + def dump(self, check_mode=False): + result = self.module_backend.dump(include_certificate=self.return_content) + result.update({ + 'changed': self.changed, + 'filename': self.path, + }) + if self.backup_file: + result['backup_file'] = self.backup_file + return result + + +def main(): + argument_spec = get_certificate_argument_spec() + add_acme_provider_to_argument_spec(argument_spec) + add_entrust_provider_to_argument_spec(argument_spec) + add_ownca_provider_to_argument_spec(argument_spec) + add_selfsigned_provider_to_argument_spec(argument_spec) + argument_spec.argument_spec.update(dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + path=dict(type='path', required=True), + backup=dict(type='bool', default=False), + return_content=dict(type='bool', default=False), + )) + argument_spec.required_if.append(['state', 'present', ['provider']]) + module = argument_spec.create_ansible_module( + add_file_common_args=True, + supports_check_mode=True, + ) + + try: + if module.params['state'] == 'absent': + certificate = CertificateAbsent(module) + + if module.check_mode: + result = certificate.dump(check_mode=True) + result['changed'] = os.path.exists(module.params['path']) + module.exit_json(**result) + + certificate.remove(module) + + else: + 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 + ) + + provider = module.params['provider'] + provider_map = { + 'acme': AcmeCertificateProvider, + 'entrust': EntrustCertificateProvider, + 'ownca': OwnCACertificateProvider, + 'selfsigned': SelfSignedCertificateProvider, + } + + backend = module.params['select_crypto_backend'] + module_backend = select_backend(module, backend, provider_map[provider]()) + certificate = GenericCertificate(module, module_backend) + certificate.generate(module) + + result = certificate.dump() + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/crypto/plugins/modules/x509_certificate_info.py b/ansible_collections/community/crypto/plugins/modules/x509_certificate_info.py new file mode 100644 index 000000000..145cd2195 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/x509_certificate_info.py @@ -0,0 +1,467 @@ +#!/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 LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: x509_certificate_info +short_description: Provide information of OpenSSL X.509 certificates +description: + - This module allows one to query information on OpenSSL certificates. + - It uses the cryptography python library to interact with OpenSSL. + - Note that this module was called C(openssl_certificate_info) when included directly in Ansible + up to version 2.9. When moved to the collection C(community.crypto), it was renamed to + M(community.crypto.x509_certificate_info). From Ansible 2.10 on, it can still be used by the + old short name (or by C(ansible.builtin.openssl_certificate_info)), which redirects to + C(community.crypto.x509_certificate_info). When using FQCNs or when using the + L(collections,https://docs.ansible.com/ansible/latest/user_guide/collections_using.html#using-collections-in-a-playbook) + keyword, the new name M(community.crypto.x509_certificate_info) should be used to avoid + a deprecation warning. +requirements: + - cryptography >= 1.6 +author: + - Felix Fontein (@felixfontein) + - Yanis Guenane (@Spredzy) + - Markus Teufelberger (@MarkusTeufelberger) +extends_documentation_fragment: + - community.crypto.attributes + - community.crypto.attributes.info_module + - community.crypto.name_encoding +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. + - PEM and DER formats are supported. + 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: '1.0.0' + 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]) (for example C(+32w1d2h)), and ASN.1 TIME (in other words, 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. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + type: str + default: auto + choices: [ auto, cryptography ] + +notes: + - All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern. + They are all in UTC. +seealso: + - module: community.crypto.x509_certificate + - module: community.crypto.x509_certificate_pipe + - ref: community.crypto.x509_certificate_info filter <ansible_collections.community.crypto.x509_certificate_info_filter> + # - plugin: community.crypto.x509_certificate_info + # plugin_type: filter + description: A filter variant of this module. +''' + +EXAMPLES = r''' +- name: Generate a Self Signed OpenSSL certificate + community.crypto.x509_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 + community.crypto.x509_certificate_info: + path: /etc/ssl/crt/ansible.com.crt + register: result + +- name: Dump information + ansible.builtin.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 (x509_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 + community.crypto.x509_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 + ansible.builtin.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 (in other words, 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. + - B(Note) that depending on the C(cryptography) version used, it is + not possible to extract the ASN.1 content of the extension, but only + to provide the re-encoded content of the extension in case it was + parsed by C(cryptography). This should usually result in exactly the + same value, except if the original extension value was malformed. + 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. + - See I(name_encoding) for how IDNs are handled. + 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(true) 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"]] +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"]] +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_type: + description: + - The certificate's public 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 + version_added: 1.7.0 + sample: RSA +public_key_data: + description: + - Public key data. Depends on the public key's type. + returned: success + type: dict + version_added: 1.7.0 + contains: + size: + description: + - Bit size of modulus (RSA) or prime number (DSA). + type: int + returned: When C(public_key_type=RSA) or C(public_key_type=DSA) + modulus: + description: + - The RSA key's modulus. + type: int + returned: When C(public_key_type=RSA) + exponent: + description: + - The RSA key's public exponent. + type: int + returned: When C(public_key_type=RSA) + p: + description: + - The C(p) value for DSA. + - This is the prime modulus upon which arithmetic takes place. + type: int + returned: When C(public_key_type=DSA) + q: + description: + - The C(q) value for DSA. + - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the + multiplicative group of the prime field used. + type: int + returned: When C(public_key_type=DSA) + g: + description: + - The C(g) value for DSA. + - This is the element spanning the subgroup of the multiplicative group of the prime field used. + type: int + returned: When C(public_key_type=DSA) + curve: + description: + - The curve's name for ECC. + type: str + returned: When C(public_key_type=ECC) + exponent_size: + description: + - The maximum number of bits of a private key. This is basically the bit size of the subgroup used. + type: int + returned: When C(public_key_type=ECC) + x: + description: + - The C(x) coordinate for the public point on the elliptic curve. + type: int + returned: When C(public_key_type=ECC) + y: + description: + - For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. + - For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. + type: int + returned: When C(public_key_type=DSA) or C(public_key_type=ECC) +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..." +fingerprints: + description: + - Fingerprints of the DER-encoded form of the whole certificate. + - 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..." + version_added: 1.2.0 +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 + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' +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 + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' +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. + - See I(name_encoding) for how IDNs are handled. + returned: success + type: list + elements: str + sample: ["DNS:www.ansible.com", "IP:1.2.3.4"] +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 + type: int + sample: 12345 +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 +issuer_uri: + description: The Issuer URI, if included in the certificate. Will be + C(none) if no issuer URI is included. + returned: success + type: str + version_added: 2.9.0 +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import string_types +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + get_relative_time_option, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import ( + select_backend, +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + path=dict(type='path'), + content=dict(type='str'), + valid_at=dict(type='dict'), + name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']), + ), + required_one_of=( + ['path', 'content'], + ), + mutually_exclusive=( + ['path', 'content'], + ), + supports_check_mode=True, + ) + + if module.params['content'] is not None: + data = module.params['content'].encode('utf-8') + else: + try: + with open(module.params['path'], 'rb') as f: + data = f.read() + except (IOError, OSError) as e: + module.fail_json(msg='Error while reading certificate file from disk: {0}'.format(e)) + + backend, module_backend = select_backend(module, module.params['select_crypto_backend'], data) + + valid_at = module.params['valid_at'] + if valid_at: + for k, v in valid_at.items(): + if not isinstance(v, string_types): + module.fail_json( + msg='The value for valid_at.{0} must be of type string (got {1})'.format(k, type(v)) + ) + valid_at[k] = get_relative_time_option(v, 'valid_at.{0}'.format(k)) + + try: + result = module_backend.get_info(der_support_enabled=module.params['content'] is None) + + not_before = module_backend.get_not_before() + not_after = module_backend.get_not_after() + + result['valid_at'] = dict() + if valid_at: + for k, v in valid_at.items(): + result['valid_at'][k] = not_before <= v <= not_after + + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/crypto/plugins/modules/x509_certificate_pipe.py b/ansible_collections/community/crypto/plugins/modules/x509_certificate_pipe.py new file mode 100644 index 000000000..440a2cdf1 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/x509_certificate_pipe.py @@ -0,0 +1,211 @@ +#!/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> +# Copyright (2) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: x509_certificate_pipe +short_description: Generate and/or check OpenSSL certificates +version_added: 1.3.0 +description: + - It implements a notion of provider (ie. C(selfsigned), C(ownca), C(entrust)) + for your certificate. + - "Please note that the module regenerates an existing certificate if it does not match the module's + options, or if it seems to be corrupt. If you are concerned that this could overwrite + your existing certificate, consider using the I(backup) option." +author: + - Yanis Guenane (@Spredzy) + - Markus Teufelberger (@MarkusTeufelberger) + - Felix Fontein (@felixfontein) +extends_documentation_fragment: + - community.crypto.attributes + - community.crypto.module_certificate + - community.crypto.module_certificate.backend_entrust_documentation + - community.crypto.module_certificate.backend_ownca_documentation + - community.crypto.module_certificate.backend_selfsigned_documentation +attributes: + check_mode: + support: full + diff_mode: + support: full +options: + provider: + description: + - Name of the provider to use to generate/retrieve the OpenSSL certificate. + - "The C(entrust) provider requires credentials for the + L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API." + type: str + choices: [ entrust, ownca, selfsigned ] + required: true + + content: + description: + - The existing certificate. + type: str + +seealso: + - module: community.crypto.x509_certificate +''' + +EXAMPLES = r''' +- name: Generate a Self Signed OpenSSL certificate + community.crypto.x509_certificate_pipe: + provider: selfsigned + privatekey_path: /etc/ssl/private/ansible.com.pem + csr_path: /etc/ssl/csr/ansible.com.csr + register: result +- name: Print the certificate + ansible.builtin.debug: + var: result.certificate + +# In the following example, both CSR and certificate file are stored on the +# machine where ansible-playbook is executed, while the OwnCA data (certificate, +# private key) are stored on the remote machine. + +- name: (1/2) Generate an OpenSSL Certificate with the CSR provided inline + community.crypto.x509_certificate_pipe: + provider: ownca + content: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.crt') }}" + csr_content: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.csr') }}" + ownca_cert: /path/to/ca_cert.crt + ownca_privatekey: /path/to/ca_cert.key + ownca_privatekey_passphrase: hunter2 + register: result + +- name: (2/2) Store certificate + ansible.builtin.copy: + dest: /etc/ssl/csr/www.ansible.com.crt + content: "{{ result.certificate }}" + delegate_to: localhost + when: result is changed + +# In the following example, the certificate from another machine is signed by +# our OwnCA whose private key and certificate are only available on this +# machine (where ansible-playbook is executed), without having to write +# the certificate file to disk on localhost. The CSR could have been +# provided by community.crypto.openssl_csr_pipe earlier, or also have been +# read from the remote machine. + +- name: (1/3) Read certificate's contents from remote machine + ansible.builtin.slurp: + src: /etc/ssl/csr/www.ansible.com.crt + register: certificate_content + +- name: (2/3) Generate an OpenSSL Certificate with the CSR provided inline + community.crypto.x509_certificate_pipe: + provider: ownca + content: "{{ certificate_content.content | b64decode }}" + csr_content: "{{ the_csr }}" + ownca_cert: /path/to/ca_cert.crt + ownca_privatekey: /path/to/ca_cert.key + ownca_privatekey_passphrase: hunter2 + delegate_to: localhost + register: result + +- name: (3/3) Store certificate + ansible.builtin.copy: + dest: /etc/ssl/csr/www.ansible.com.crt + content: "{{ result.certificate }}" + when: result is changed +''' + +RETURN = r''' +certificate: + description: The (current or generated) certificate's content. + returned: changed or success + type: str +''' + + +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( + select_backend, + get_certificate_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_entrust import ( + EntrustCertificateProvider, + add_entrust_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_ownca import ( + OwnCACertificateProvider, + add_ownca_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_selfsigned import ( + SelfSignedCertificateProvider, + add_selfsigned_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + + +class GenericCertificate(object): + """Retrieve a certificate using the given module backend.""" + def __init__(self, module, module_backend): + self.check_mode = module.check_mode + self.module_backend = module_backend + self.changed = False + if module.params['content'] is not None: + self.module_backend.set_existing(module.params['content'].encode('utf-8')) + + def generate(self, module): + if self.module_backend.needs_regeneration(): + if not self.check_mode: + self.module_backend.generate_certificate() + self.changed = True + + def dump(self, check_mode=False): + result = self.module_backend.dump(include_certificate=True) + result.update({ + 'changed': self.changed, + }) + return result + + +def main(): + argument_spec = get_certificate_argument_spec() + argument_spec.argument_spec['provider']['required'] = True + add_entrust_provider_to_argument_spec(argument_spec) + add_ownca_provider_to_argument_spec(argument_spec) + add_selfsigned_provider_to_argument_spec(argument_spec) + argument_spec.argument_spec.update(dict( + content=dict(type='str'), + )) + module = argument_spec.create_ansible_module( + supports_check_mode=True, + ) + + try: + provider = module.params['provider'] + provider_map = { + 'entrust': EntrustCertificateProvider, + 'ownca': OwnCACertificateProvider, + 'selfsigned': SelfSignedCertificateProvider, + } + + backend = module.params['select_crypto_backend'] + module_backend = select_backend(module, backend, provider_map[provider]()) + certificate = GenericCertificate(module, module_backend) + certificate.generate(module) + result = certificate.dump() + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/crypto/plugins/modules/x509_crl.py b/ansible_collections/community/crypto/plugins/modules/x509_crl.py new file mode 100644 index 000000000..824ed8310 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/x509_crl.py @@ -0,0 +1,945 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2019, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: x509_crl +version_added: '1.0.0' +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 by 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) +extends_documentation_fragment: + - ansible.builtin.files + - community.crypto.attributes + - community.crypto.attributes.files + - community.crypto.name_encoding +attributes: + check_mode: + support: full + diff_mode: + support: full + safe_file_operations: + support: full +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 ] + + crl_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(true). + - The default value is C(generate). + - This parameter was called I(mode) before community.crypto 2.13.0. It has been renamed to avoid + a collision with the common I(mode) parameter for setting the CRL file's access mode. + type: str + # default: generate + choices: [ generate, update ] + version_added: 2.13.0 + mode: + description: + - This parameter has been renamed to I(crl_mode). The old name I(mode) is now deprecated and will + be removed in community.crypto 3.0.0. Replace usage of this parameter with I(crl_mode). + - Note that from community.crypto 3.0.0 on, I(mode) will be used for the CRL file's mode. + type: str + # default: generate + choices: [ generate, update ] + + force: + description: + - Should the CRL be forced to be regenerated. + type: bool + default: false + + 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: false + + path: + description: + - Remote absolute path where the generated CRL file should be created or is already located. + type: path + required: true + + format: + description: + - Whether the CRL file should be in PEM or DER format. + - If an existing CRL file does match everything but I(format), it will be converted to the correct format + instead of regenerated. + type: str + choices: [pem, der] + default: pem + + 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. + - If the order of the components is important, use I(issuer_ordered). + - One of I(issuer) and I(issuer_ordered) is required if I(state) is C(present). + - Mutually exclusive with I(issuer_ordered). + type: dict + issuer_ordered: + description: + - A list of dictionaries, where every dictionary must contain one key/value pair. + This key/value pair will be present in the issuer name field of the CRL. + - If you want to specify more than one value with the same key in a row, you can + use a list as value. + - One of I(issuer) and I(issuer_ordered) is required if I(state) is C(present). + - Mutually exclusive with I(issuer). + type: list + elements: dict + version_added: 2.0.0 + + 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]) (for example C(+32w1d2h)). + - Note that if using relative time this module is NOT idempotent, except when + I(ignore_timestamps) is set to C(true). + 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]) (for example C(+32w1d2h)). + - Note that if using relative time this module is NOT idempotent, except when + I(ignore_timestamps) is set to C(true). + - 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]) (for example C(+32w1d2h)). + - Note that if using relative time this module is NOT idempotent, except when + I(ignore_timestamps) is set to C(true). + 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: false + 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: false + 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]) (for example 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(true). + type: str + invalidity_date_critical: + description: + - Whether the invalidity date extension should be critical. + type: bool + default: false + + 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: false + + return_content: + description: + - If set to C(true), will return the (current or generated) CRL's content as I(crl). + type: bool + default: false + +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 + community.crypto.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(true) + 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 +format: + description: + - Whether the CRL is in PEM format (C(pem)) or in DER format (C(der)). + returned: success + type: str + sample: pem +issuer: + description: + - The CRL's issuer. + - Note that for repeated values, only the last one will be returned. + - See I(name_encoding) for how IDNs are handled. + 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. + - See I(name_encoding) for how IDNs are handled. + type: list + elements: str + sample: ["DNS:ca.example.org"] + issuer_critical: + description: Whether the certificate issuer extension is critical. + type: bool + sample: false + 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: false + 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: false +crl: + description: + - The (current or generated) CRL's content. + - Will be the CRL itself if I(format) is C(pem), and Base64 of the + CRL if I(format) is C(der). + returned: if I(state) is C(present) and I(return_content) is C(true) + type: str +''' + + +import base64 +import os +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.text.converters import to_native, to_text + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.io import ( + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_privatekey, + load_certificate, + parse_name_field, + parse_ordered_name_field, + get_relative_time_option, + select_message_digest, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_decode_name, + cryptography_get_name, + cryptography_key_needs_digest_for_signing, + cryptography_name_to_oid, + cryptography_oid_to_name, + cryptography_serial_number_of_cert, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import ( + REVOCATION_REASON_MAP, + TIMESTAMP_FORMAT, + cryptography_decode_revoked_certificate, + cryptography_dump_revoked, + cryptography_get_signature_algorithm_oid_from_crl, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + identify_pem_format, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import ( + get_crl_info, +) + +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 + + +class CRLError(OpenSSLObjectError): + pass + + +class CRL(OpenSSLObject): + + def __init__(self, module): + super(CRL, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode + ) + + self.format = module.params['format'] + + self.update = module.params['crl_mode'] == 'update' + self.ignore_timestamps = module.params['ignore_timestamps'] + self.return_content = module.params['return_content'] + self.name_encoding = module.params['name_encoding'] + 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'] + + try: + if module.params['issuer_ordered']: + self.issuer_ordered = True + self.issuer = parse_ordered_name_field(module.params['issuer_ordered'], 'issuer_ordered') + else: + self.issuer_ordered = False + self.issuer = parse_name_field(module.params['issuer'], 'issuer') + except (TypeError, ValueError) as exc: + module.fail_json(msg=to_native(exc)) + + self.last_update = get_relative_time_option(module.params['last_update'], 'last_update') + self.next_update = get_relative_time_option(module.params['next_update'], 'next_update') + + self.digest = 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 = load_certificate(rc['path'], content=rc['content'], backend='cryptography') + result['serial_number'] = cryptography_serial_number_of_cert(cert) + except 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'] = [cryptography_get_name(issuer, 'issuer') for issuer in rc['issuer']] + result['issuer_critical'] = rc['issuer_critical'] + result['revocation_date'] = get_relative_time_option( + rc['revocation_date'], + path_prefix + 'revocation_date' + ) + if rc['reason']: + result['reason'] = REVOCATION_REASON_MAP[rc['reason']] + result['reason_critical'] = rc['reason_critical'] + if rc['invalidity_date']: + result['invalidity_date'] = 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 = load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend='cryptography' + ) + except OpenSSLBadPassphraseError as exc: + raise CRLError(exc) + + self.crl = None + try: + with open(self.path, 'rb') as f: + data = f.read() + self.actual_format = 'pem' if identify_pem_format(data) else 'der' + if self.actual_format == 'pem': + self.crl = x509.load_pem_x509_crl(data, default_backend()) + if self.return_content: + self.crl_content = data + else: + self.crl = x509.load_der_x509_crl(data, default_backend()) + if self.return_content: + self.crl_content = base64.b64encode(data) + except Exception as dummy: + self.crl_content = None + self.actual_format = self.format + data = None + + self.diff_after = self.diff_before = self._get_info(data) + + def _get_info(self, data): + if data is None: + return dict() + try: + result = get_crl_info(self.module, data) + result['can_parse_crl'] = True + return result + except Exception as exc: + return dict(can_parse_crl=False) + + 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): + issuer = None + if entry['issuer'] is not None: + # Normalize to IDNA. If this is used-provided, it was already converted to + # IDNA (by cryptography_get_name) and thus the `idna` library is present. + # If this is coming from cryptography and isn't already in IDNA (i.e. ascii), + # cryptography < 2.1 must be in use, which depends on `idna`. So this should + # not require `idna` except if it was already used by code earlier during + # this invocation. + issuer = tuple(cryptography_decode_name(issuer, idn_rewrite='idna') for issuer in entry['issuer']) + if self.ignore_timestamps: + # Throw out revocation_date + return ( + entry['serial_number'], + issuer, + entry['issuer_critical'], + entry['reason'], + entry['reason_critical'], + entry['invalidity_date'], + entry['invalidity_date_critical'], + ) + else: + return ( + entry['serial_number'], + entry['revocation_date'], + issuer, + entry['issuer_critical'], + entry['reason'], + entry['reason_critical'], + entry['invalidity_date'], + entry['invalidity_date_critical'], + ) + + def check(self, module, perms_required=True, ignore_conversion=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 cryptography_key_needs_digest_for_signing(self.privatekey): + if self.crl.signature_hash_algorithm is None or self.digest.name != self.crl.signature_hash_algorithm.name: + return False + else: + if self.crl.signature_hash_algorithm is not None: + return False + + want_issuer = [(cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer] + is_issuer = [(sub.oid, sub.value) for sub in self.crl.issuer] + if not self.issuer_ordered: + want_issuer = set(want_issuer) + is_issuer = set(is_issuer) + if want_issuer != is_issuer: + return False + + old_entries = [self._compress_entry(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 do not 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 + + if self.format != self.actual_format and not ignore_conversion: + return False + + return True + + def _generate_crl(self): + backend = default_backend() + crl = CertificateRevocationListBuilder() + + try: + crl = crl.issuer_name(Name([ + NameAttribute(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(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(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)) + + digest = None + if cryptography_key_needs_digest_for_signing(self.privatekey): + digest = self.digest + self.crl = crl.sign(self.privatekey, digest, backend=backend) + if self.format == 'pem': + return self.crl.public_bytes(Encoding.PEM) + else: + return self.crl.public_bytes(Encoding.DER) + + def generate(self): + result = None + if not self.check(self.module, perms_required=False, ignore_conversion=True) or self.force: + result = self._generate_crl() + elif not self.check(self.module, perms_required=False, ignore_conversion=False) and self.crl: + if self.format == 'pem': + result = self.crl.public_bytes(Encoding.PEM) + else: + result = self.crl.public_bytes(Encoding.DER) + + if result is not None: + self.diff_after = self._get_info(result) + if self.return_content: + if self.format == 'pem': + self.crl_content = result + else: + self.crl_content = base64.b64encode(result) + if self.backup: + self.backup_file = self.module.backup_local(self.path) + write_file(self.module, result) + self.changed = True + + file_args = self.module.load_file_common_arguments(self.module.params) + if self.module.check_file_absent_if_check_mode(file_args['path']): + self.changed = True + elif self.module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + def dump(self, check_mode=False): + result = { + 'changed': self.changed, + 'filename': self.path, + 'privatekey': self.privatekey_path, + 'format': self.format, + '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'] = 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(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding)) + elif self.crl: + result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT) + result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT) + result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl)) + issuer = [] + for attribute in self.crl.issuer: + issuer.append([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 = cryptography_decode_revoked_certificate(cert) + result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding)) + + if self.return_content: + result['crl'] = self.crl_content + + result['diff'] = dict( + before=self.diff_before, + after=self.diff_after, + ) + return result + + +def main(): + module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + crl_mode=dict( + type='str', + # default='generate', + choices=['generate', 'update'], + ), + mode=dict( + type='str', + # default='generate', + choices=['generate', 'update'], + removed_in_version='3.0.0', + removed_from_collection='community.crypto', + ), + force=dict(type='bool', default=False), + backup=dict(type='bool', default=False), + path=dict(type='path', required=True), + format=dict(type='str', default='pem', choices=['pem', 'der']), + privatekey_path=dict(type='path'), + privatekey_content=dict(type='str', no_log=True), + privatekey_passphrase=dict(type='str', no_log=True), + issuer=dict(type='dict'), + issuer_ordered=dict(type='list', elements='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']], + ), + name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']), + ), + required_if=[ + ('state', 'present', ['privatekey_path', 'privatekey_content'], True), + ('state', 'present', ['issuer', 'issuer_ordered'], True), + ('state', 'present', ['next_update', 'revoked_certificates'], False), + ], + mutually_exclusive=( + ['privatekey_path', 'privatekey_content'], + ['issuer', 'issuer_ordered'], + ), + supports_check_mode=True, + add_file_common_args=True, + ) + + if module.params['mode']: + if module.params['crl_mode']: + module.fail_json('You cannot use both `mode` and `crl_mode`. Use `crl_mode`.') + module.params['crl_mode'] = module.params['mode'] + # TODO: in 3.0.0, once the option `mode` has been removed, remove this: + module.params.pop('mode', None) + # From then on, `mode` will be the file mode of the CRL file + + 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) or not crl.check(module, ignore_conversion=False) + 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 OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/crypto/plugins/modules/x509_crl_info.py b/ansible_collections/community/crypto/plugins/modules/x509_crl_info.py new file mode 100644 index 000000000..7b0ebcac9 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/modules/x509_crl_info.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: x509_crl_info +version_added: '1.0.0' +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) +extends_documentation_fragment: + - community.crypto.attributes + - community.crypto.attributes.info_module + - community.crypto.name_encoding +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 CRL in PEM format, or Base64-encoded X.509 CRL. + - Either I(path) or I(content) must be specified, but not both. + type: str + list_revoked_certificates: + description: + - If set to C(false), the list of revoked certificates is not included in the result. + - This is useful when retrieving information on large CRL files. Enumerating all revoked + certificates can take some time, including serializing the result as JSON, sending it to + the Ansible controller, and decoding it again. + type: bool + default: true + version_added: 1.7.0 + +notes: + - All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern. + They are all in UTC. +seealso: + - module: community.crypto.x509_crl + - ref: community.crypto.x509_crl_info filter <ansible_collections.community.crypto.x509_crl_info_filter> + # - plugin: community.crypto.x509_crl_info + # plugin_type: filter + description: A filter variant of this module. +''' + +EXAMPLES = r''' +- name: Get information on CRL + community.crypto.x509_crl_info: + path: /etc/ssl/my-ca.crl + register: result + +- name: Print the information + ansible.builtin.debug: + msg: "{{ result }}" + +- name: Get information on CRL without list of revoked certificates + community.crypto.x509_crl_info: + path: /etc/ssl/very-large.crl + list_revoked_certificates: false + register: result +''' + +RETURN = r''' +format: + description: + - Whether the CRL is in PEM format (C(pem)) or in DER format (C(der)). + returned: success + type: str + sample: pem +issuer: + description: + - The CRL's issuer. + - Note that for repeated values, only the last one will be returned. + - See I(name_encoding) for how IDNs are handled. + 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 if I(list_revoked_certificates=true) + 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. + - See I(name_encoding) for how IDNs are handled. + type: list + elements: str + sample: ["DNS:ca.example.org"] + issuer_critical: + description: Whether the certificate issuer extension is critical. + type: bool + sample: false + 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: false + 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: false +''' + + +import base64 +import binascii + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + identify_pem_format, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import ( + get_crl_info, +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + path=dict(type='path'), + content=dict(type='str'), + list_revoked_certificates=dict(type='bool', default=True), + name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']), + ), + required_one_of=( + ['path', 'content'], + ), + mutually_exclusive=( + ['path', 'content'], + ), + supports_check_mode=True, + ) + + if module.params['content'] is None: + try: + with open(module.params['path'], 'rb') as f: + data = f.read() + except (IOError, OSError) as e: + module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e)) + else: + data = module.params['content'].encode('utf-8') + if not identify_pem_format(data): + try: + data = base64.b64decode(module.params['content']) + except (binascii.Error, TypeError) as e: + module.fail_json(msg='Error while Base64 decoding content: {0}'.format(e)) + + try: + result = get_crl_info(module, data, list_revoked_certificates=module.params['list_revoked_certificates']) + module.exit_json(**result) + except OpenSSLObjectError as e: + module.fail_json(msg=to_native(e)) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/crypto/plugins/plugin_utils/action_module.py b/ansible_collections/community/crypto/plugins/plugin_utils/action_module.py new file mode 100644 index 000000000..3d7a77b20 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/plugin_utils/action_module.py @@ -0,0 +1,765 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2012-2013 Michael DeHaan <michael.dehaan@gmail.com> +# Copyright (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com> +# Copyright (c) 2019 Ansible Project +# Copyright (c) 2020 Felix Fontein <felix@fontein.de> +# Copyright (c) 2021 Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Parts taken from ansible.module_utils.basic and ansible.module_utils.common.warnings. + +# NOTE: THIS IS ONLY FOR ACTION PLUGINS! + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc +import copy +import traceback + +from ansible import constants as C +from ansible.errors import AnsibleError +from ansible.module_utils import six +from ansible.module_utils.basic import AnsibleFallbackNotFound, SEQUENCETYPE, remove_values +from ansible.module_utils.common._collections_compat import ( + Mapping +) +from ansible.module_utils.common.parameters import ( + PASS_VARS, + PASS_BOOLS, +) +from ansible.module_utils.common.validation import ( + check_mutually_exclusive, + check_required_arguments, + check_required_by, + check_required_if, + check_required_one_of, + check_required_together, + count_terms, + check_type_bool, + check_type_bits, + check_type_bytes, + check_type_float, + check_type_int, + check_type_jsonarg, + check_type_list, + check_type_dict, + check_type_path, + check_type_raw, + check_type_str, + safe_eval, +) +from ansible.module_utils.common.text.formatters import ( + lenient_lowercase, +) +from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE +from ansible.module_utils.six import ( + binary_type, + string_types, + text_type, +) +from ansible.module_utils.common.text.converters import to_native, to_text +from ansible.plugins.action import ActionBase + + +try: + # For ansible-core 2.11, we can use the ArgumentSpecValidator. We also import + # ModuleArgumentSpecValidator since that indicates that the 'classical' approach + # will no longer work. + from ansible.module_utils.common.arg_spec import ( # noqa: F401, pylint: disable=unused-import + ArgumentSpecValidator, + ModuleArgumentSpecValidator, # ModuleArgumentSpecValidator is not used + ) + from ansible.module_utils.errors import UnsupportedError + HAS_ARGSPEC_VALIDATOR = True +except ImportError: + # For ansible-base 2.10 and Ansible 2.9, we need to use the 'classical' approach + from ansible.module_utils.common.parameters import ( + handle_aliases, + list_deprecations, + list_no_log_values, + ) + HAS_ARGSPEC_VALIDATOR = False + + +class _ModuleExitException(Exception): + def __init__(self, result): + super(_ModuleExitException, self).__init__() + self.result = result + + +class AnsibleActionModule(object): + def __init__(self, action_plugin, argument_spec, bypass_checks=False, + mutually_exclusive=None, required_together=None, + required_one_of=None, supports_check_mode=False, + required_if=None, required_by=None): + # Internal data + self.__action_plugin = action_plugin + self.__warnings = [] + self.__deprecations = [] + + # AnsibleModule data + self._name = self.__action_plugin._task.action + self.argument_spec = argument_spec + self.supports_check_mode = supports_check_mode + self.check_mode = self.__action_plugin._play_context.check_mode + self.bypass_checks = bypass_checks + self.no_log = self.__action_plugin._play_context.no_log + + self.mutually_exclusive = mutually_exclusive + self.required_together = required_together + self.required_one_of = required_one_of + self.required_if = required_if + self.required_by = required_by + self._diff = self.__action_plugin._play_context.diff + self._verbosity = self.__action_plugin._display.verbosity + + self.aliases = {} + self._legal_inputs = [] + self._options_context = list() + + self.params = copy.deepcopy(self.__action_plugin._task.args) + self.no_log_values = set() + if HAS_ARGSPEC_VALIDATOR: + self._validator = ArgumentSpecValidator( + self.argument_spec, + self.mutually_exclusive, + self.required_together, + self.required_one_of, + self.required_if, + self.required_by, + ) + self._validation_result = self._validator.validate(self.params) + self.params.update(self._validation_result.validated_parameters) + self.no_log_values.update(self._validation_result._no_log_values) + + try: + error = self._validation_result.errors[0] + except IndexError: + error = None + + # We cannot use ModuleArgumentSpecValidator directly since it uses mechanisms for reporting + # warnings and deprecations that do not work in plugins. This is a copy of that code adjusted + # for our use-case: + for d in self._validation_result._deprecations: + # Before ansible-core 2.14.2, deprecations were always for aliases: + if 'name' in d: + self.deprecate( + "Alias '{name}' is deprecated. See the module docs for more information".format(name=d['name']), + version=d.get('version'), date=d.get('date'), collection_name=d.get('collection_name')) + # Since ansible-core 2.14.2, a message is present that can be directly printed: + if 'msg' in d: + self.deprecate(d['msg'], version=d.get('version'), date=d.get('date'), collection_name=d.get('collection_name')) + + for w in self._validation_result._warnings: + self.warn('Both option {option} and its alias {alias} are set.'.format(option=w['option'], alias=w['alias'])) + + # Fail for validation errors, even in check mode + if error: + msg = self._validation_result.errors.msg + if isinstance(error, UnsupportedError): + msg = "Unsupported parameters for ({name}) {kind}: {msg}".format(name=self._name, kind='module', msg=msg) + + self.fail_json(msg=msg) + else: + self._set_fallbacks() + + # append to legal_inputs and then possibly check against them + try: + self.aliases = self._handle_aliases() + except (ValueError, TypeError) as e: + # Use exceptions here because it is not safe to call fail_json until no_log is processed + raise _ModuleExitException(dict(failed=True, msg="Module alias error: %s" % to_native(e))) + + # Save parameter values that should never be logged + self._handle_no_log_values() + + self._check_arguments() + + # check exclusive early + if not bypass_checks: + self._check_mutually_exclusive(mutually_exclusive) + + self._set_defaults(pre=True) + + self._CHECK_ARGUMENT_TYPES_DISPATCHER = { + 'str': self._check_type_str, + 'list': check_type_list, + 'dict': check_type_dict, + 'bool': check_type_bool, + 'int': check_type_int, + 'float': check_type_float, + 'path': check_type_path, + 'raw': check_type_raw, + 'jsonarg': check_type_jsonarg, + 'json': check_type_jsonarg, + 'bytes': check_type_bytes, + 'bits': check_type_bits, + } + if not bypass_checks: + self._check_required_arguments() + self._check_argument_types() + self._check_argument_values() + self._check_required_together(required_together) + self._check_required_one_of(required_one_of) + self._check_required_if(required_if) + self._check_required_by(required_by) + + self._set_defaults(pre=False) + + # deal with options sub-spec + self._handle_options() + + def _handle_aliases(self, spec=None, param=None, option_prefix=''): + if spec is None: + spec = self.argument_spec + if param is None: + param = self.params + + # this uses exceptions as it happens before we can safely call fail_json + alias_warnings = [] + alias_results, self._legal_inputs = handle_aliases(spec, param, alias_warnings=alias_warnings) # pylint: disable=used-before-assignment + for option, alias in alias_warnings: + self.warn('Both option %s and its alias %s are set.' % (option_prefix + option, option_prefix + alias)) + + deprecated_aliases = [] + for i in spec.keys(): + if 'deprecated_aliases' in spec[i].keys(): + for alias in spec[i]['deprecated_aliases']: + deprecated_aliases.append(alias) + + for deprecation in deprecated_aliases: + if deprecation['name'] in param.keys(): + self.deprecate("Alias '%s' is deprecated. See the module docs for more information" % deprecation['name'], + version=deprecation.get('version'), date=deprecation.get('date'), + collection_name=deprecation.get('collection_name')) + return alias_results + + def _handle_no_log_values(self, spec=None, param=None): + if spec is None: + spec = self.argument_spec + if param is None: + param = self.params + + try: + self.no_log_values.update(list_no_log_values(spec, param)) # pylint: disable=used-before-assignment + except TypeError as te: + self.fail_json(msg="Failure when processing no_log parameters. Module invocation will be hidden. " + "%s" % to_native(te), invocation={'module_args': 'HIDDEN DUE TO FAILURE'}) + + for message in list_deprecations(spec, param): # pylint: disable=used-before-assignment + self.deprecate(message['msg'], version=message.get('version'), date=message.get('date'), + collection_name=message.get('collection_name')) + + def _check_arguments(self, spec=None, param=None, legal_inputs=None): + self._syslog_facility = 'LOG_USER' + unsupported_parameters = set() + if spec is None: + spec = self.argument_spec + if param is None: + param = self.params + if legal_inputs is None: + legal_inputs = self._legal_inputs + + for k in list(param.keys()): + + if k not in legal_inputs: + unsupported_parameters.add(k) + + for k in PASS_VARS: + # handle setting internal properties from internal ansible vars + param_key = '_ansible_%s' % k + if param_key in param: + if k in PASS_BOOLS: + setattr(self, PASS_VARS[k][0], self.boolean(param[param_key])) + else: + setattr(self, PASS_VARS[k][0], param[param_key]) + + # clean up internal top level params: + if param_key in self.params: + del self.params[param_key] + else: + # use defaults if not already set + if not hasattr(self, PASS_VARS[k][0]): + setattr(self, PASS_VARS[k][0], PASS_VARS[k][1]) + + if unsupported_parameters: + msg = "Unsupported parameters for (%s) module: %s" % (self._name, ', '.join(sorted(list(unsupported_parameters)))) + if self._options_context: + msg += " found in %s." % " -> ".join(self._options_context) + supported_parameters = list() + for key in sorted(spec.keys()): + if 'aliases' in spec[key] and spec[key]['aliases']: + supported_parameters.append("%s (%s)" % (key, ', '.join(sorted(spec[key]['aliases'])))) + else: + supported_parameters.append(key) + msg += " Supported parameters include: %s" % (', '.join(supported_parameters)) + self.fail_json(msg=msg) + if self.check_mode and not self.supports_check_mode: + self.exit_json(skipped=True, msg="action module (%s) does not support check mode" % self._name) + + def _count_terms(self, check, param=None): + if param is None: + param = self.params + return count_terms(check, param) + + def _check_mutually_exclusive(self, spec, param=None): + if param is None: + param = self.params + + try: + check_mutually_exclusive(spec, param) + except TypeError as e: + msg = to_native(e) + if self._options_context: + msg += " found in %s" % " -> ".join(self._options_context) + self.fail_json(msg=msg) + + def _check_required_one_of(self, spec, param=None): + if spec is None: + return + + if param is None: + param = self.params + + try: + check_required_one_of(spec, param) + except TypeError as e: + msg = to_native(e) + if self._options_context: + msg += " found in %s" % " -> ".join(self._options_context) + self.fail_json(msg=msg) + + def _check_required_together(self, spec, param=None): + if spec is None: + return + if param is None: + param = self.params + + try: + check_required_together(spec, param) + except TypeError as e: + msg = to_native(e) + if self._options_context: + msg += " found in %s" % " -> ".join(self._options_context) + self.fail_json(msg=msg) + + def _check_required_by(self, spec, param=None): + if spec is None: + return + if param is None: + param = self.params + + try: + check_required_by(spec, param) + except TypeError as e: + self.fail_json(msg=to_native(e)) + + def _check_required_arguments(self, spec=None, param=None): + if spec is None: + spec = self.argument_spec + if param is None: + param = self.params + + try: + check_required_arguments(spec, param) + except TypeError as e: + msg = to_native(e) + if self._options_context: + msg += " found in %s" % " -> ".join(self._options_context) + self.fail_json(msg=msg) + + def _check_required_if(self, spec, param=None): + ''' ensure that parameters which conditionally required are present ''' + if spec is None: + return + if param is None: + param = self.params + + try: + check_required_if(spec, param) + except TypeError as e: + msg = to_native(e) + if self._options_context: + msg += " found in %s" % " -> ".join(self._options_context) + self.fail_json(msg=msg) + + def _check_argument_values(self, spec=None, param=None): + ''' ensure all arguments have the requested values, and there are no stray arguments ''' + if spec is None: + spec = self.argument_spec + if param is None: + param = self.params + for (k, v) in spec.items(): + choices = v.get('choices', None) + if choices is None: + continue + if isinstance(choices, SEQUENCETYPE) and not isinstance(choices, (binary_type, text_type)): + if k in param: + # Allow one or more when type='list' param with choices + if isinstance(param[k], list): + diff_list = ", ".join([item for item in param[k] if item not in choices]) + if diff_list: + choices_str = ", ".join([to_native(c) for c in choices]) + msg = "value of %s must be one or more of: %s. Got no match for: %s" % (k, choices_str, diff_list) + if self._options_context: + msg += " found in %s" % " -> ".join(self._options_context) + self.fail_json(msg=msg) + elif param[k] not in choices: + # PyYaml converts certain strings to bools. If we can unambiguously convert back, do so before checking + # the value. If we cannot figure this out, module author is responsible. + lowered_choices = None + if param[k] == 'False': + lowered_choices = lenient_lowercase(choices) + overlap = BOOLEANS_FALSE.intersection(choices) + if len(overlap) == 1: + # Extract from a set + (param[k],) = overlap + + if param[k] == 'True': + if lowered_choices is None: + lowered_choices = lenient_lowercase(choices) + overlap = BOOLEANS_TRUE.intersection(choices) + if len(overlap) == 1: + (param[k],) = overlap + + if param[k] not in choices: + choices_str = ", ".join([to_native(c) for c in choices]) + msg = "value of %s must be one of: %s, got: %s" % (k, choices_str, param[k]) + if self._options_context: + msg += " found in %s" % " -> ".join(self._options_context) + self.fail_json(msg=msg) + else: + msg = "internal error: choices for argument %s are not iterable: %s" % (k, choices) + if self._options_context: + msg += " found in %s" % " -> ".join(self._options_context) + self.fail_json(msg=msg) + + def safe_eval(self, value, locals=None, include_exceptions=False): + return safe_eval(value, locals, include_exceptions) + + def _check_type_str(self, value, param=None, prefix=''): + opts = { + 'error': False, + 'warn': False, + 'ignore': True + } + + # Ignore, warn, or error when converting to a string. + allow_conversion = opts.get(C.STRING_CONVERSION_ACTION, True) + try: + return check_type_str(value, allow_conversion) + except TypeError: + common_msg = 'quote the entire value to ensure it does not change.' + from_msg = '{0!r}'.format(value) + to_msg = '{0!r}'.format(to_text(value)) + + if param is not None: + if prefix: + param = '{0}{1}'.format(prefix, param) + + from_msg = '{0}: {1!r}'.format(param, value) + to_msg = '{0}: {1!r}'.format(param, to_text(value)) + + if C.STRING_CONVERSION_ACTION == 'error': + msg = common_msg.capitalize() + raise TypeError(to_native(msg)) + elif C.STRING_CONVERSION_ACTION == 'warn': + msg = ('The value "{0}" (type {1.__class__.__name__}) was converted to "{2}" (type string). ' + 'If this does not look like what you expect, {3}').format(from_msg, value, to_msg, common_msg) + self.warn(to_native(msg)) + return to_native(value, errors='surrogate_or_strict') + + def _handle_options(self, argument_spec=None, params=None, prefix=''): + ''' deal with options to create sub spec ''' + if argument_spec is None: + argument_spec = self.argument_spec + if params is None: + params = self.params + + for (k, v) in argument_spec.items(): + wanted = v.get('type', None) + if wanted == 'dict' or (wanted == 'list' and v.get('elements', '') == 'dict'): + spec = v.get('options', None) + if v.get('apply_defaults', False): + if spec is not None: + if params.get(k) is None: + params[k] = {} + else: + continue + elif spec is None or k not in params or params[k] is None: + continue + + self._options_context.append(k) + + if isinstance(params[k], dict): + elements = [params[k]] + else: + elements = params[k] + + for idx, param in enumerate(elements): + if not isinstance(param, dict): + self.fail_json(msg="value of %s must be of type dict or list of dict" % k) + + new_prefix = prefix + k + if wanted == 'list': + new_prefix += '[%d]' % idx + new_prefix += '.' + + self._set_fallbacks(spec, param) + options_aliases = self._handle_aliases(spec, param, option_prefix=new_prefix) + + options_legal_inputs = list(spec.keys()) + list(options_aliases.keys()) + + self._check_arguments(spec, param, options_legal_inputs) + + # check exclusive early + if not self.bypass_checks: + self._check_mutually_exclusive(v.get('mutually_exclusive', None), param) + + self._set_defaults(pre=True, spec=spec, param=param) + + if not self.bypass_checks: + self._check_required_arguments(spec, param) + self._check_argument_types(spec, param, new_prefix) + self._check_argument_values(spec, param) + + self._check_required_together(v.get('required_together', None), param) + self._check_required_one_of(v.get('required_one_of', None), param) + self._check_required_if(v.get('required_if', None), param) + self._check_required_by(v.get('required_by', None), param) + + self._set_defaults(pre=False, spec=spec, param=param) + + # handle multi level options (sub argspec) + self._handle_options(spec, param, new_prefix) + self._options_context.pop() + + def _get_wanted_type(self, wanted, k): + if not callable(wanted): + if wanted is None: + # Mostly we want to default to str. + # For values set to None explicitly, return None instead as + # that allows a user to unset a parameter + wanted = 'str' + try: + type_checker = self._CHECK_ARGUMENT_TYPES_DISPATCHER[wanted] + except KeyError: + self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k)) + else: + # set the type_checker to the callable, and reset wanted to the callable's name (or type if it does not have one, ala MagicMock) + type_checker = wanted + wanted = getattr(wanted, '__name__', to_native(type(wanted))) + + return type_checker, wanted + + def _handle_elements(self, wanted, param, values): + type_checker, wanted_name = self._get_wanted_type(wanted, param) + validated_params = [] + # Get param name for strings so we can later display this value in a useful error message if needed + # Only pass 'kwargs' to our checkers and ignore custom callable checkers + kwargs = {} + if wanted_name == 'str' and isinstance(wanted, string_types): + if isinstance(param, string_types): + kwargs['param'] = param + elif isinstance(param, dict): + kwargs['param'] = list(param.keys())[0] + for value in values: + try: + validated_params.append(type_checker(value, **kwargs)) + except (TypeError, ValueError) as e: + msg = "Elements value for option %s" % param + if self._options_context: + msg += " found in '%s'" % " -> ".join(self._options_context) + msg += " is of type %s and we were unable to convert to %s: %s" % (type(value), wanted_name, to_native(e)) + self.fail_json(msg=msg) + return validated_params + + def _check_argument_types(self, spec=None, param=None, prefix=''): + ''' ensure all arguments have the requested type ''' + + if spec is None: + spec = self.argument_spec + if param is None: + param = self.params + + for (k, v) in spec.items(): + wanted = v.get('type', None) + if k not in param: + continue + + value = param[k] + if value is None: + continue + + type_checker, wanted_name = self._get_wanted_type(wanted, k) + # Get param name for strings so we can later display this value in a useful error message if needed + # Only pass 'kwargs' to our checkers and ignore custom callable checkers + kwargs = {} + if wanted_name == 'str' and isinstance(type_checker, string_types): + kwargs['param'] = list(param.keys())[0] + + # Get the name of the parent key if this is a nested option + if prefix: + kwargs['prefix'] = prefix + + try: + param[k] = type_checker(value, **kwargs) + wanted_elements = v.get('elements', None) + if wanted_elements: + if wanted != 'list' or not isinstance(param[k], list): + msg = "Invalid type %s for option '%s'" % (wanted_name, param) + if self._options_context: + msg += " found in '%s'." % " -> ".join(self._options_context) + msg += ", elements value check is supported only with 'list' type" + self.fail_json(msg=msg) + param[k] = self._handle_elements(wanted_elements, k, param[k]) + + except (TypeError, ValueError) as e: + msg = "argument %s is of type %s" % (k, type(value)) + if self._options_context: + msg += " found in '%s'." % " -> ".join(self._options_context) + msg += " and we were unable to convert to %s: %s" % (wanted_name, to_native(e)) + self.fail_json(msg=msg) + + def _set_defaults(self, pre=True, spec=None, param=None): + if spec is None: + spec = self.argument_spec + if param is None: + param = self.params + for (k, v) in spec.items(): + default = v.get('default', None) + if pre is True: + # this prevents setting defaults on required items + if default is not None and k not in param: + param[k] = default + else: + # make sure things without a default still get set None + if k not in param: + param[k] = default + + def _set_fallbacks(self, spec=None, param=None): + if spec is None: + spec = self.argument_spec + if param is None: + param = self.params + + for (k, v) in spec.items(): + fallback = v.get('fallback', (None,)) + fallback_strategy = fallback[0] + fallback_args = [] + fallback_kwargs = {} + if k not in param and fallback_strategy is not None: + for item in fallback[1:]: + if isinstance(item, dict): + fallback_kwargs = item + else: + fallback_args = item + try: + param[k] = fallback_strategy(*fallback_args, **fallback_kwargs) + except AnsibleFallbackNotFound: + continue + + def warn(self, warning): + # Copied from ansible.module_utils.common.warnings: + if isinstance(warning, string_types): + self.__warnings.append(warning) + else: + raise TypeError("warn requires a string not a %s" % type(warning)) + + def deprecate(self, msg, version=None, date=None, collection_name=None): + if version is not None and date is not None: + raise AssertionError("implementation error -- version and date must not both be set") + + # Copied from ansible.module_utils.common.warnings: + if isinstance(msg, string_types): + # For compatibility, we accept that neither version nor date is set, + # and treat that the same as if version would haven been set + if date is not None: + self.__deprecations.append({'msg': msg, 'date': date, 'collection_name': collection_name}) + else: + self.__deprecations.append({'msg': msg, 'version': version, 'collection_name': collection_name}) + else: + raise TypeError("deprecate requires a string not a %s" % type(msg)) + + def _return_formatted(self, kwargs): + if 'invocation' not in kwargs: + kwargs['invocation'] = {'module_args': self.params} + + if 'warnings' in kwargs: + if isinstance(kwargs['warnings'], list): + for w in kwargs['warnings']: + self.warn(w) + else: + self.warn(kwargs['warnings']) + + if self.__warnings: + kwargs['warnings'] = self.__warnings + + if 'deprecations' in kwargs: + if isinstance(kwargs['deprecations'], list): + for d in kwargs['deprecations']: + if isinstance(d, SEQUENCETYPE) and len(d) == 2: + self.deprecate(d[0], version=d[1]) + elif isinstance(d, Mapping): + self.deprecate(d['msg'], version=d.get('version'), date=d.get('date'), + collection_name=d.get('collection_name')) + else: + self.deprecate(d) # pylint: disable=ansible-deprecated-no-version + else: + self.deprecate(kwargs['deprecations']) # pylint: disable=ansible-deprecated-no-version + + if self.__deprecations: + kwargs['deprecations'] = self.__deprecations + + kwargs = remove_values(kwargs, self.no_log_values) + raise _ModuleExitException(kwargs) + + def exit_json(self, **kwargs): + result = dict(kwargs) + if 'failed' not in result: + result['failed'] = False + self._return_formatted(result) + + def fail_json(self, msg, **kwargs): + result = dict(kwargs) + result['failed'] = True + result['msg'] = msg + self._return_formatted(result) + + +@six.add_metaclass(abc.ABCMeta) +class ActionModuleBase(ActionBase): + @abc.abstractmethod + def setup_module(self): + """Return pair (ArgumentSpec, kwargs).""" + pass + + @abc.abstractmethod + def run_module(self, module): + """Run module code""" + module.fail_json(msg='Not implemented.') + + def run(self, tmp=None, task_vars=None): + if task_vars is None: + task_vars = dict() + + result = super(ActionModuleBase, self).run(tmp, task_vars) + del tmp # tmp no longer has any effect + + try: + argument_spec, kwargs = self.setup_module() + module = argument_spec.create_ansible_module_helper(AnsibleActionModule, (self, ), **kwargs) + self.run_module(module) + raise AnsibleError('Internal error: action module did not call module.exit_json()') + except _ModuleExitException as mee: + result.update(mee.result) + return result + except Exception as dummy: + result['failed'] = True + result['msg'] = 'MODULE FAILURE' + result['exception'] = traceback.format_exc() + return result diff --git a/ansible_collections/community/crypto/plugins/plugin_utils/filter_module.py b/ansible_collections/community/crypto/plugins/plugin_utils/filter_module.py new file mode 100644 index 000000000..ce58317e6 --- /dev/null +++ b/ansible_collections/community/crypto/plugins/plugin_utils/filter_module.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# NOTE: THIS IS ONLY FOR FILTER PLUGINS! + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible.errors import AnsibleFilterError + + +class FilterModuleMock(object): + def __init__(self, params): + self.check_mode = True + self.params = params + self._diff = False + + def fail_json(self, msg, **kwargs): + raise AnsibleFilterError(msg) diff --git a/ansible_collections/community/crypto/tests/.gitignore b/ansible_collections/community/crypto/tests/.gitignore new file mode 100644 index 000000000..6edf5dc10 --- /dev/null +++ b/ansible_collections/community/crypto/tests/.gitignore @@ -0,0 +1,5 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +output/ diff --git a/ansible_collections/community/crypto/tests/config.yml b/ansible_collections/community/crypto/tests/config.yml new file mode 100644 index 000000000..38590f2e4 --- /dev/null +++ b/ansible_collections/community/crypto/tests/config.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# See template for more information: +# https://github.com/ansible/ansible/blob/devel/test/lib/ansible_test/config/config.yml +modules: + python_requires: default diff --git a/ansible_collections/community/crypto/tests/ee/all.yml b/ansible_collections/community/crypto/tests/ee/all.yml new file mode 100644 index 000000000..28aa0f5e7 --- /dev/null +++ b/ansible_collections/community/crypto/tests/ee/all.yml @@ -0,0 +1,48 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- hosts: localhost + tasks: + - name: Show Python info + debug: + var: ansible_python + + - name: Register cryptography version + command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'" + register: cryptography_version + + - name: Register pyOpenSSL version + command: "{{ ansible_python.executable }} -c 'import OpenSSL; print(OpenSSL.__version__)'" + ignore_errors: true + register: pyopenssl_version + + - name: Determine output directory + set_fact: + output_path: "{{ 'output-%0x' % ((2**32) | random) }}" + + - name: Find all roles + ansible.builtin.find: + paths: + - "{{ (playbook_dir | default('.')) ~ '/roles' }}" + file_type: directory + depth: 1 + register: result + + - name: Create output directory + ansible.builtin.file: + path: "{{ output_path }}" + state: directory + + - block: + - name: Include all roles + ansible.builtin.include_role: + name: "{{ item }}" + loop: "{{ result.files | map(attribute='path') | map('regex_replace', '.*/', '') | sort }}" + + always: + - name: Remove output directory + ansible.builtin.file: + path: "{{ output_path }}" + state: absent diff --git a/ansible_collections/community/crypto/tests/ee/roles/crypto_info/tasks/main.yml b/ansible_collections/community/crypto/tests/ee/roles/crypto_info/tasks/main.yml new file mode 100644 index 000000000..76cecf25b --- /dev/null +++ b/ansible_collections/community/crypto/tests/ee/roles/crypto_info/tasks/main.yml @@ -0,0 +1,31 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Run crypto_info + community.crypto.crypto_info: + register: result + +- name: Dump result + debug: + var: result + +- name: Validate result + assert: + that: + - result.openssl_present + - result.python_cryptography_installed + - result.python_cryptography_capabilities.has_dsa + - result.python_cryptography_capabilities.has_dsa_sign + - result.python_cryptography_capabilities.has_ec + - result.python_cryptography_capabilities.has_ec_sign + - result.python_cryptography_capabilities.has_ed25519 + - result.python_cryptography_capabilities.has_ed25519_sign + - result.python_cryptography_capabilities.has_ed448 + - result.python_cryptography_capabilities.has_ed448_sign + - result.python_cryptography_capabilities.has_rsa + - result.python_cryptography_capabilities.has_rsa_sign + - result.python_cryptography_capabilities.has_x25519 + - result.python_cryptography_capabilities.has_x25519_serialization + - result.python_cryptography_capabilities.has_x448 diff --git a/ansible_collections/community/crypto/tests/ee/roles/luks_device/tasks/main.yml b/ansible_collections/community/crypto/tests/ee/roles/luks_device/tasks/main.yml new file mode 100644 index 000000000..410a8e59c --- /dev/null +++ b/ansible_collections/community/crypto/tests/ee/roles/luks_device/tasks/main.yml @@ -0,0 +1,49 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Run cryptsetup (smoke test) + ansible.builtin.command: cryptsetup --version + +- name: Determine cryptfile path + ansible.builtin.set_fact: + cryptfile_path: "{{ output_path }}/cryptfile" + keyfile_path: "{{ output_path }}/keyfile" + +- name: Create cryptfile + ansible.builtin.command: dd if=/dev/zero of={{ cryptfile_path }} bs=1M count=32 + +- name: Create keyfile + ansible.builtin.copy: + dest: "{{ keyfile_path }}" + content: hunter2 + +- # Creating devices doesn't work well. We will have to try this again when luks_device + # supports working with container files directly. + when: false + block: + - name: Create lookback device + command: losetup -f {{ cryptfile_path }} + + - name: Determine loop device name + command: losetup -j {{ cryptfile_path }} --output name + register: cryptfile_device_output + + - set_fact: + cryptfile_device: "{{ cryptfile_device_output.stdout_lines[1] }}" + + - name: Create LUKS container + community.crypto.luks_device: + device: "{{ cryptfile_device }}" + # device: "{{ cryptfile_path }}" + state: present + keyfile: "{{ keyfile_path }}" + pbkdf: + iteration_time: 0.1 + + - name: Destroy LUKS container + community.crypto.luks_device: + device: "{{ cryptfile_device }}" + # device: "{{ cryptfile_path }}" + state: absent diff --git a/ansible_collections/community/crypto/tests/ee/roles/openssh_keypair/tasks/main.yml b/ansible_collections/community/crypto/tests/ee/roles/openssh_keypair/tasks/main.yml new file mode 100644 index 000000000..27c24934b --- /dev/null +++ b/ansible_collections/community/crypto/tests/ee/roles/openssh_keypair/tasks/main.yml @@ -0,0 +1,17 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Generate key with OpenSSH binary backend + community.crypto.openssh_keypair: + path: "{{ output_path }}/openssh-key-1" + size: 2048 + backend: opensshbin + +- name: Generate key with cryptography backend + community.crypto.openssh_keypair: + path: "{{ output_path }}/openssh-key-2" + size: 2048 + backend: cryptography + when: cryptography_version.stdout is ansible.builtin.version('3.0', '>=') diff --git a/ansible_collections/community/crypto/tests/ee/roles/openssl_pkcs12/tasks/main.yml b/ansible_collections/community/crypto/tests/ee/roles/openssl_pkcs12/tasks/main.yml new file mode 100644 index 000000000..2fd8edac0 --- /dev/null +++ b/ansible_collections/community/crypto/tests/ee/roles/openssl_pkcs12/tasks/main.yml @@ -0,0 +1,46 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create private key + community.crypto.openssl_privatekey: + path: "{{ output_path }}/pkcs12-cert.key" + type: ECC + curve: secp256r1 + +- name: Create CSR + community.crypto.openssl_csr: + path: "{{ output_path }}/pkcs12-cert.csr" + privatekey_path: "{{ output_path }}/pkcs12-cert.key" + +- name: Create certificate + community.crypto.x509_certificate: + path: "{{ output_path }}/pkcs12-cert.pem" + csr_path: "{{ output_path }}/pkcs12-cert.csr" + privatekey_path: "{{ output_path }}/pkcs12-cert.key" + provider: selfsigned + +- name: Create PKCS#12 with cryptography backend + community.crypto.openssl_pkcs12: + action: export + path: "{{ output_path }}/pkcs12-1.p12" + mode: '0644' + friendly_name: foo + privatekey_path: "{{ output_path }}/pkcs12-cert.key" + certificate_path: "{{ output_path }}/pkcs12-cert.pem" + state: present + select_crypto_backend: cryptography + when: cryptography_version.stdout is ansible.builtin.version('3.0', '>=') + +- name: Create PKCS#12 with PyOpenSSL backend + community.crypto.openssl_pkcs12: + action: export + path: "{{ output_path }}/pkcs12-2.p12" + mode: '0644' + friendly_name: foo + privatekey_path: "{{ output_path }}/pkcs12-cert.key" + certificate_path: "{{ output_path }}/pkcs12-cert.pem" + state: present + select_crypto_backend: pyopenssl + when: not (has_no_pyopenssl | default(false)) diff --git a/ansible_collections/community/crypto/tests/ee/roles/openssl_privatekey/tasks/main.yml b/ansible_collections/community/crypto/tests/ee/roles/openssl_privatekey/tasks/main.yml new file mode 100644 index 000000000..d6929fc48 --- /dev/null +++ b/ansible_collections/community/crypto/tests/ee/roles/openssl_privatekey/tasks/main.yml @@ -0,0 +1,15 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create RSA private key + community.crypto.openssl_privatekey: + path: "{{ output_path }}/privatekey-1" + size: 2048 + +- name: Create ECC private key + community.crypto.openssl_privatekey: + path: "{{ output_path }}/privatekey-2" + type: ECC + curve: secp256r1 diff --git a/ansible_collections/community/crypto/tests/ee/roles/smoke/library/smoke_ipaddress.py b/ansible_collections/community/crypto/tests/ee/roles/smoke/library/smoke_ipaddress.py new file mode 100644 index 000000000..6c2156135 --- /dev/null +++ b/ansible_collections/community/crypto/tests/ee/roles/smoke/library/smoke_ipaddress.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: smoke_ipaddress +short_description: Check whether ipaddress is present +author: + - Felix Fontein (@felixfontein) +description: + - Check whether C(ipaddress) is present. +options: {} +''' + +EXAMPLES = r''' # ''' + +RETURN = r''' # ''' + +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +try: + import ipaddress # noqa: F401, pylint: disable=unused-import + HAS_IPADDRESS = True + IPADDRESS_IMP_ERR = None +except ImportError as exc: + IPADDRESS_IMP_ERR = traceback.format_exc() + HAS_IPADDRESS = False + + +def main(): + module = AnsibleModule(argument_spec=dict(), supports_check_mode=True) + + if not HAS_IPADDRESS: + module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMP_ERR) + + module.exit_json(msg='Everything is ok') + + +if __name__ == '__main__': # pragma: no cover + main() # pragma: no cover diff --git a/ansible_collections/community/crypto/tests/ee/roles/smoke/library/smoke_pyyaml.py b/ansible_collections/community/crypto/tests/ee/roles/smoke/library/smoke_pyyaml.py new file mode 100644 index 000000000..457176c91 --- /dev/null +++ b/ansible_collections/community/crypto/tests/ee/roles/smoke/library/smoke_pyyaml.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: smoke_pyyaml +short_description: Check whether PyYAML is present +author: + - Felix Fontein (@felixfontein) +description: + - Check whether C(yaml) is present. +options: {} +''' + +EXAMPLES = r''' # ''' + +RETURN = r''' # ''' + +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +try: + import yaml # noqa: F401, pylint: disable=unused-import + HAS_PYYAML = True + PYYAML_IMP_ERR = None +except ImportError as exc: + PYYAML_IMP_ERR = traceback.format_exc() + HAS_PYYAML = False + + +def main(): + module = AnsibleModule(argument_spec=dict(), supports_check_mode=True) + + if not HAS_PYYAML: + module.fail_json(msg=missing_required_lib('PyYAML'), exception=PYYAML_IMP_ERR) + + module.exit_json(msg='Everything is ok') + + +if __name__ == '__main__': # pragma: no cover + main() # pragma: no cover diff --git a/ansible_collections/community/crypto/tests/ee/roles/smoke/tasks/main.yml b/ansible_collections/community/crypto/tests/ee/roles/smoke/tasks/main.yml new file mode 100644 index 000000000..1e8b659bf --- /dev/null +++ b/ansible_collections/community/crypto/tests/ee/roles/smoke/tasks/main.yml @@ -0,0 +1,22 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Check whether ipaddress is present + smoke_ipaddress: + register: result + +- name: Validate result + assert: + that: + - result.msg == 'Everything is ok' + +- name: Check whether PyYAML is present + smoke_pyyaml: + register: result + +- name: Validate result + assert: + that: + - result.msg == 'Everything is ok' diff --git a/ansible_collections/community/crypto/tests/ee/roles/x509_certificate/tasks/main.yml b/ansible_collections/community/crypto/tests/ee/roles/x509_certificate/tasks/main.yml new file mode 100644 index 000000000..23e03a868 --- /dev/null +++ b/ansible_collections/community/crypto/tests/ee/roles/x509_certificate/tasks/main.yml @@ -0,0 +1,22 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create private key + community.crypto.openssl_privatekey: + path: "{{ output_path }}/cert.key" + type: ECC + curve: secp256r1 + +- name: Create CSR + community.crypto.openssl_csr: + path: "{{ output_path }}/cert.csr" + privatekey_path: "{{ output_path }}/cert.key" + +- name: Create certificate + community.crypto.x509_certificate: + path: "{{ output_path }}/cert.pem" + csr_path: "{{ output_path }}/cert.csr" + privatekey_path: "{{ output_path }}/cert.key" + provider: selfsigned diff --git a/ansible_collections/community/crypto/tests/integration/requirements.yml b/ansible_collections/community/crypto/tests/integration/requirements.yml new file mode 100644 index 000000000..524cb7d97 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/requirements.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +collections: +- community.general +- community.internal_test_tools diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_account/aliases b/ansible_collections/community/crypto/tests/integration/targets/acme_account/aliases new file mode 100644 index 000000000..b7f6d4f48 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_account/aliases @@ -0,0 +1,10 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/1 +azp/posix/1 +cloud/acme + +# For some reason connecting to helper containers does not work on the Alpine VMs +skip/alpine diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_account/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_account/meta/main.yml new file mode 100644 index 000000000..2e8ad10b8 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_account/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_acme + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_account/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_account/tasks/impl.yml new file mode 100644 index 000000000..79fd43ebd --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_account/tasks/impl.yml @@ -0,0 +1,308 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- block: + - name: Generate account keys + openssl_privatekey: + path: "{{ remote_tmp_dir }}/{{ item.name }}.pem" + passphrase: "{{ item.pass | default(omit) | default(omit, true) }}" + cipher: "{{ 'auto' if (item.pass | default(false)) else omit }}" + type: ECC + curve: secp256r1 + force: true + loop: "{{ account_keys }}" + + - name: Parse account keys (to ease debugging some test failures) + openssl_privatekey_info: + path: "{{ remote_tmp_dir }}/{{ item.name }}.pem" + passphrase: "{{ item.pass | default(omit) | default(omit, true) }}" + return_private_key_data: true + loop: "{{ account_keys }}" + + vars: + account_keys: + - name: accountkey + - name: accountkey2 + pass: "{{ 'hunter2' if select_crypto_backend != 'openssl' else '' }}" + - name: accountkey3 + - name: accountkey4 + - name: accountkey5 + +- name: Do not try to create account + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + allow_creation: false + ignore_errors: true + register: account_not_created + +- name: Create it now (check mode, diff) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + allow_creation: true + terms_agreed: true + contact: + - mailto:example@example.org + check_mode: true + diff: true + register: account_created_check + +- name: Create it now + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + allow_creation: true + terms_agreed: true + contact: + - mailto:example@example.org + register: account_created + +- name: Create it now (idempotent) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + allow_creation: true + terms_agreed: true + contact: + - mailto:example@example.org + register: account_created_idempotent + +- name: Read account key + slurp: + src: '{{ remote_tmp_dir }}/accountkey.pem' + register: slurp + +- name: Change email address (check mode, diff) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_content: "{{ slurp.content | b64decode }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + # allow_creation: false + contact: + - mailto:example@example.com + check_mode: true + diff: true + register: account_modified_check + +- name: Change email address + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_content: "{{ slurp.content | b64decode }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + # allow_creation: false + contact: + - mailto:example@example.com + register: account_modified + +- name: Change email address (idempotent) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + account_uri: "{{ account_created.account_uri }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + # allow_creation: false + 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: "{{ remote_tmp_dir }}/accountkey.pem" + account_uri: "{{ account_created.account_uri ~ '12345thisdoesnotexist' }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + contact: [] + ignore_errors: true + register: account_modified_wrong_uri + +- name: Clear contact email addresses (check mode, diff) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + # allow_creation: false + contact: [] + check_mode: true + diff: true + register: account_modified_2_check + +- name: Clear contact email addresses + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + # allow_creation: false + contact: [] + register: account_modified_2 + +- name: Clear contact email addresses (idempotent) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + # allow_creation: false + contact: [] + register: account_modified_2_idempotent + +- name: Change account key (check mode, diff) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + new_account_key_src: "{{ remote_tmp_dir }}/accountkey2.pem" + new_account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}" + state: changed_key + contact: + - mailto:example@example.com + check_mode: true + diff: true + register: account_change_key_check + +- name: Change account key + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + new_account_key_src: "{{ remote_tmp_dir }}/accountkey2.pem" + new_account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}" + 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: "{{ remote_tmp_dir }}/accountkey2.pem" + account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: absent + check_mode: true + diff: true + register: account_deactivate_check + +- name: Deactivate account + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey2.pem" + account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: absent + register: account_deactivate + +- name: Deactivate account (idempotent) + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey2.pem" + account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + 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: "{{ remote_tmp_dir }}/accountkey2.pem" + account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + allow_creation: false + ignore_errors: true + register: account_not_created_2 + +- name: Do not try to create account III + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + allow_creation: false + ignore_errors: true + register: account_not_created_3 + +- name: Create account with External Account Binding + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/{{ item.account }}.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + allow_creation: true + terms_agreed: true + contact: + - mailto:example@example.org + external_account_binding: + kid: "{{ item.kid }}" + alg: "{{ item.alg }}" + key: "{{ item.key }}" + register: account_created_eab + ignore_errors: true + loop: + - account: accountkey3 + kid: kid-1 + alg: HS256 + key: zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W + - account: accountkey4 + kid: kid-2 + alg: HS384 + key: b10lLJs8l1GPIzsLP0s6pMt8O0XVGnfTaCeROxQM0BIt2XrJMDHJZBM5NuQmQJQH + - account: accountkey5 + kid: kid-3 + alg: HS512 + key: zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W +- debug: var=account_created_eab diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_account/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_account/tasks/main.yml new file mode 100644 index 000000000..68d47973d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_account/tasks/main.yml @@ -0,0 +1,40 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- 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: "{{ remote_tmp_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ remote_tmp_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', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_account/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_account/tests/validate.yml new file mode 100644 index 000000000..dc927ff61 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_account/tests/validate.yml @@ -0,0 +1,141 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- 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.' + +- name: Validate that the account with External Account Binding has been created + assert: + that: + - account_created_eab.results[0] is changed + - account_created_eab.results[1] is changed + - account_created_eab.results[2] is failed + - "'HS512 key must be at least 64 bytes long' in account_created_eab.results[2].msg" diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/aliases b/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/aliases new file mode 100644 index 000000000..b7f6d4f48 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/aliases @@ -0,0 +1,10 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/1 +azp/posix/1 +cloud/acme + +# For some reason connecting to helper containers does not work on the Alpine VMs +skip/alpine diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/meta/main.yml new file mode 100644 index 000000000..2e8ad10b8 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_acme + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/tasks/impl.yml new file mode 100644 index 000000000..f1d53abe2 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/tasks/impl.yml @@ -0,0 +1,102 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- block: + - name: Generate account keys + openssl_privatekey: + path: "{{ remote_tmp_dir }}/{{ item }}.pem" + type: ECC + curve: secp256r1 + force: true + loop: "{{ account_keys }}" + + - name: Parse account keys (to ease debugging some test failures) + openssl_privatekey_info: + path: "{{ remote_tmp_dir }}/{{ item }}.pem" + return_private_key_data: true + loop: "{{ account_keys }}" + + vars: + account_keys: + - accountkey + - accountkey2 + +- name: Check that account does not exist + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + register: account_not_created + +- name: Create it now + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + allow_creation: true + terms_agreed: true + contact: + - mailto:example@example.org + +- name: Check that account exists + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + register: account_created + +- name: Read account key + slurp: + src: '{{ remote_tmp_dir }}/accountkey.pem' + register: slurp + +- name: Clear email address + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_content: "{{ slurp.content | b64decode }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + state: present + allow_creation: false + contact: [] + +- name: Check that account was modified + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + 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: "{{ remote_tmp_dir }}/accountkey.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + 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: "{{ remote_tmp_dir }}/accountkey2.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + account_uri: "{{ account_created.account_uri }}" + ignore_errors: true + register: account_wrong_key diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/tasks/main.yml new file mode 100644 index 000000000..68d47973d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/tasks/main.yml @@ -0,0 +1,40 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- 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: "{{ remote_tmp_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ remote_tmp_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', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/tests/validate.yml new file mode 100644 index 000000000..3730599cd --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_account_info/tests/validate.yml @@ -0,0 +1,44 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- 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 diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/aliases b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/aliases new file mode 100644 index 000000000..b7f6d4f48 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/aliases @@ -0,0 +1,10 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/1 +azp/posix/1 +cloud/acme + +# For some reason connecting to helper containers does not work on the Alpine VMs +skip/alpine diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/meta/main.yml new file mode 100644 index 000000000..d71644584 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/meta/main.yml @@ -0,0 +1,10 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_acme + - setup_pyopenssl # needed for Ubuntu 16.04 + - setup_remote_tmp_dir + - prepare_jinja2_compat diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/tasks/impl.yml new file mode 100644 index 000000000..5d2ab5b99 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/tasks/impl.yml @@ -0,0 +1,509 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +## SET UP ACCOUNT KEYS ######################################################################## +- block: + - name: Generate account keys + openssl_privatekey: + path: "{{ remote_tmp_dir }}/{{ item.name }}.pem" + type: "{{ item.type }}" + size: "{{ item.size | default(omit) }}" + curve: "{{ item.curve | default(omit) }}" + force: true + loop: "{{ account_keys }}" + + vars: + account_keys: + - name: account-ec256 + type: ECC + curve: secp256r1 + - name: account-ec384 + type: ECC + curve: secp384r1 + - name: account-rsa + type: RSA + size: "{{ default_rsa_key_size }}" +## 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: false + account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem" + state: absent +- name: Read account key (EC384) + slurp: + src: '{{ remote_tmp_dir }}/account-ec384.pem' + register: slurp +- name: Create ECC384 account + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + account_key_content: "{{ slurp.content | b64decode }}" + state: present + allow_creation: true + terms_agreed: true + contact: + - mailto:example@example.org + - mailto:example@example.com +- name: Create RSA account + acme_account: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + account_key_src: "{{ remote_tmp_dir }}/account-rsa.pem" + state: present + allow_creation: true + terms_agreed: true + 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: "{{ default_rsa_key_size }}" + subject_alt_name: "DNS:example.com" + subject_alt_name_critical: false + account_key: account-ec256 + challenge: http-01 + modify_account: true + deactivate_authzs: false + force: false + remaining_days: 10 + terms_agreed: true + account_email: "example@example.org" + retrieve_all_alternates: true + acme_expected_root_number: 1 + select_chain: + - test_certificates: last + issuer: "{{ acme_roots[1].subject }}" + use_csr_content: true +- 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 + certificate_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else '' }}" + key_type: ec256 + subject_alt_name: "DNS:*.example.com,DNS:example.com" + subject_alt_name_critical: true + account_key: account-ec384 + challenge: dns-01 + modify_account: false + deactivate_authzs: true + force: false + remaining_days: 10 + terms_agreed: false + account_email: "" + acme_expected_root_number: 0 + retrieve_all_alternates: true + 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 }}" + use_csr_content: false +- 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: Read account key (RSA) + slurp: + src: '{{ remote_tmp_dir }}/account-rsa.pem' + register: slurp_account_key +- 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: false + account_key_content: "{{ slurp_account_key.content | b64decode }}" + challenge: dns-01 + modify_account: false + deactivate_authzs: false + force: false + remaining_days: 10 + terms_agreed: false + account_email: "" + acme_expected_root_number: 0 + retrieve_all_alternates: true + select_chain: + - test_certificates: last + subject: "{{ acme_roots[1].subject }}" + use_csr_content: true +- 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: "{{ default_rsa_key_size }}" + 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: false + account_key: account-rsa + challenge: http-01 + modify_account: false + deactivate_authzs: true + force: true + remaining_days: 10 + terms_agreed: false + 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 }}" + use_csr_content: false +- 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: false + account_key: account-ec384 + challenge: http-01 + modify_account: false + deactivate_authzs: true + force: true + remaining_days: 10 + terms_agreed: false + account_email: "" + use_csr_content: true +- 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: false + account_key: account-ec384 + challenge: http-01 + modify_account: false + deactivate_authzs: true + force: false + remaining_days: 10 + terms_agreed: false + account_email: "" + use_csr_content: false +- 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: false + account_key: account-ec384 + challenge: http-01 + modify_account: false + deactivate_authzs: true + force: true + remaining_days: 1000 + terms_agreed: false + account_email: "" + use_csr_content: true +- 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: Read account key (EC384) + slurp: + src: '{{ remote_tmp_dir }}/account-ec384.pem' + register: slurp_account_key +- 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: false + account_key_content: "{{ slurp_account_key.content | b64decode }}" + challenge: http-01 + modify_account: false + deactivate_authzs: true + force: true + remaining_days: 10 + terms_agreed: false + account_email: "" + use_csr_content: false +- name: Store obtain results for cert 5d + set_fact: + cert_5_recreate_3: "{{ challenge_data is changed }}" + cert_5d_obtain_results: "{{ certificate_obtain_result }}" +- block: + - name: Obtain cert 6 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 6 + certificate_name: cert-6 + key_type: rsa + rsa_bits: "{{ default_rsa_key_size }}" + subject_alt_name: "DNS:example.org" + subject_alt_name_critical: false + account_key: account-ec256 + challenge: tls-alpn-01 + modify_account: true + deactivate_authzs: false + force: false + remaining_days: 10 + terms_agreed: true + 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: first + subject_key_identifier: "{{ acme_intermediates[0].subject_key_identifier }}" + - test_certificates: last + issuer: "{{ acme_roots[1].subject }}" + use_csr_content: true + - 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 }}" + when: acme_intermediates[0].subject_key_identifier is defined +- block: + - name: Obtain cert 7 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 7 + certificate_name: cert-7 + key_type: rsa + rsa_bits: "{{ default_rsa_key_size }}" + subject_alt_name: + - "IP:127.0.0.1" + # - "IP:::1" + subject_alt_name_critical: false + account_key: account-ec256 + challenge: http-01 + modify_account: true + deactivate_authzs: false + force: false + remaining_days: 10 + terms_agreed: true + account_email: "example@example.org" + acme_expected_root_number: 2 + select_chain: + - test_certificates: last + authority_key_identifier: "{{ acme_roots[2].subject_key_identifier }}" + use_csr_content: false + - 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 }}" + when: acme_roots[2].subject_key_identifier is defined +- block: + - name: Obtain cert 8 + include_tasks: obtain-cert.yml + vars: + certgen_title: Certificate 8 + certificate_name: cert-8 + key_type: rsa + rsa_bits: "{{ default_rsa_key_size }}" + 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: false + account_key: account-ec256 + challenge: tls-alpn-01 + challenge_alpn_tls: acme_challenge_cert_helper + modify_account: true + deactivate_authzs: false + force: false + remaining_days: 10 + terms_agreed: true + account_email: "example@example.org" + use_csr_content: true + - 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 }}" + when: cryptography_version.stdout is version('1.3', '>=') +## DISSECT CERTIFICATES ####################################################################### +# Make sure certificates are valid. Root certificate for Pebble equals the chain certificate. +- name: Verifying cert 1 + command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-1-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-1-chain.pem" "{{ remote_tmp_dir }}/cert-1.pem"' + ignore_errors: true + register: cert_1_valid +- name: Verifying cert 2 + command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-2-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-2-chain.pem" "{{ remote_tmp_dir }}/cert-2.pem"' + ignore_errors: true + register: cert_2_valid +- name: Verifying cert 3 + command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-3-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-3-chain.pem" "{{ remote_tmp_dir }}/cert-3.pem"' + ignore_errors: true + register: cert_3_valid +- name: Verifying cert 4 + command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-4-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-4-chain.pem" "{{ remote_tmp_dir }}/cert-4.pem"' + ignore_errors: true + register: cert_4_valid +- name: Verifying cert 5 + command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-5-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-5-chain.pem" "{{ remote_tmp_dir }}/cert-5.pem"' + ignore_errors: true + register: cert_5_valid +- name: Verifying cert 6 + command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-6-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-6-chain.pem" "{{ remote_tmp_dir }}/cert-6.pem"' + ignore_errors: true + register: cert_6_valid + when: acme_intermediates[0].subject_key_identifier is defined +- name: Verifying cert 7 + command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-7-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-7-chain.pem" "{{ remote_tmp_dir }}/cert-7.pem"' + ignore_errors: true + register: cert_7_valid + when: acme_roots[2].subject_key_identifier is defined +- name: Verifying cert 8 + command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-8-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-8-chain.pem" "{{ remote_tmp_dir }}/cert-8.pem"' + ignore_errors: true + register: cert_8_valid + when: cryptography_version.stdout is version('1.3', '>=') +# Dump certificate info +- name: Dumping cert 1 + command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-1.pem" -noout -text' + register: cert_1_text +- name: Dumping cert 2 + command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-2.pem" -noout -text' + register: cert_2_text +- name: Dumping cert 3 + command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-3.pem" -noout -text' + register: cert_3_text +- name: Dumping cert 4 + command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-4.pem" -noout -text' + register: cert_4_text +- name: Dumping cert 5 + command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-5.pem" -noout -text' + register: cert_5_text +- name: Dumping cert 6 + command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-6.pem" -noout -text' + register: cert_6_text + when: acme_intermediates[0].subject_key_identifier is defined +- name: Dumping cert 7 + command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-7.pem" -noout -text' + register: cert_7_text + when: acme_roots[2].subject_key_identifier is defined +- name: Dumping cert 8 + command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-8.pem" -noout -text' + register: cert_8_text + when: cryptography_version.stdout is version('1.3', '>=') +# Dump certificate info +- name: Dumping cert 1 + x509_certificate_info: + path: "{{ remote_tmp_dir }}/cert-1.pem" + register: cert_1_info +- name: Dumping cert 2 + x509_certificate_info: + path: "{{ remote_tmp_dir }}/cert-2.pem" + register: cert_2_info +- name: Dumping cert 3 + x509_certificate_info: + path: "{{ remote_tmp_dir }}/cert-3.pem" + register: cert_3_info +- name: Dumping cert 4 + x509_certificate_info: + path: "{{ remote_tmp_dir }}/cert-4.pem" + register: cert_4_info +- name: Dumping cert 5 + x509_certificate_info: + path: "{{ remote_tmp_dir }}/cert-5.pem" + register: cert_5_info +- name: Dumping cert 6 + x509_certificate_info: + path: "{{ remote_tmp_dir }}/cert-6.pem" + register: cert_6_info + when: acme_intermediates[0].subject_key_identifier is defined +- name: Dumping cert 7 + x509_certificate_info: + path: "{{ remote_tmp_dir }}/cert-7.pem" + register: cert_7_info + when: acme_roots[2].subject_key_identifier is defined +- name: Dumping cert 8 + x509_certificate_info: + path: "{{ remote_tmp_dir }}/cert-8.pem" + register: cert_8_info + when: cryptography_version.stdout is version('1.3', '>=') +## GET ACCOUNT ORDERS ######################################################################### +- name: Don't retrieve orders + acme_account_info: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + 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: "{{ remote_tmp_dir }}/account-ec256.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + 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: "{{ remote_tmp_dir }}/account-ec384.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + 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: "{{ remote_tmp_dir }}/account-ec256.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + 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: "{{ remote_tmp_dir }}/account-ec384.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + retrieve_orders: object_list + register: account_orders_full2 diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/tasks/main.yml new file mode 100644 index 000000000..e715c7aab --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/tasks/main.yml @@ -0,0 +1,121 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- block: + - name: Obtain root and intermediate certificates + get_url: + url: "http://{{ acme_host }}:5000/{{ item.0 }}-certificate-for-ca/{{ item.1 }}" + dest: "{{ remote_tmp_dir }}/acme-{{ item.0 }}-{{ item.1 }}.pem" + loop: "{{ query('nested', types, root_numbers) }}" + + - name: Analyze root certificates + x509_certificate_info: + path: "{{ remote_tmp_dir }}/acme-root-{{ item }}.pem" + loop: "{{ root_numbers }}" + register: acme_roots + + - name: Analyze intermediate certificates + x509_certificate_info: + path: "{{ remote_tmp_dir }}/acme-intermediate-{{ item }}.pem" + loop: "{{ root_numbers }}" + register: acme_intermediates + + - name: Read root certificates + slurp: + src: "{{ remote_tmp_dir ~ '/acme-root-' ~ item ~ '.pem' }}" + loop: "{{ root_numbers }}" + register: slurp_roots + + - set_fact: + x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}" + loop: "{{ acme_roots.results }}" + register: acme_roots_tmp + + - name: Read intermediate certificates + slurp: + src: "{{ remote_tmp_dir ~ '/acme-intermediate-' ~ item ~ '.pem' }}" + loop: "{{ root_numbers }}" + register: slurp_intermediates + + - set_fact: + x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}" + 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: "{{ slurp_roots.results | map(attribute='content') | map('b64decode') | list }}" + acme_intermediates: "{{ acme_intermediates_tmp.results | map(attribute='ansible_facts.x__') | list }}" + acme_intermediate_certs: "{{ slurp_intermediates.results | map(attribute='content') | map('b64decode') | 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: "{{ remote_tmp_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ remote_tmp_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', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/tasks/obtain-cert.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/tasks/obtain-cert.yml new file mode 100644 index 000000000..6882e5339 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/tasks/obtain-cert.yml @@ -0,0 +1,159 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +## PRIVATE KEY ################################################################################ +- name: ({{ certgen_title }}) Create cert private key + openssl_privatekey: + path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" + type: "{{ 'RSA' if key_type == 'rsa' else 'ECC' }}" + size: "{{ rsa_bits if key_type == 'rsa' else omit }}" + curve: >- + {{ omit if key_type == 'rsa' else + 'secp256r1' if key_type == 'ec256' else + 'secp384r1' if key_type == 'ec384' else + 'secp521r1' if key_type == 'ec521' else + 'invalid value for key_type!' }} + passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}" + cipher: "{{ 'auto' if certificate_passphrase | default() else omit }}" + force: true +## CSR ######################################################################################## +- name: ({{ certgen_title }}) Create cert CSR + openssl_csr: + path: "{{ remote_tmp_dir }}/{{ certificate_name }}.csr" + privatekey_path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" + privatekey_passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}" + subject_alt_name: "{{ subject_alt_name }}" + subject_alt_name_critical: "{{ subject_alt_name_critical }}" + return_content: true + register: csr_result +## ACME STEP 1 ################################################################################ +- name: ({{ certgen_title }}) Obtain cert, step 1 + acme_certificate: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}" + account_key_content: "{{ account_key_content | default(omit) }}" + account_key_passphrase: "{{ account_key_passphrase | default(omit) | default(omit, true) }}" + modify_account: "{{ modify_account }}" + csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}" + csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}" + dest: "{{ remote_tmp_dir }}/{{ certificate_name }}.pem" + fullchain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-fullchain.pem" + chain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-chain.pem" + challenge: "{{ challenge }}" + deactivate_authzs: "{{ deactivate_authzs }}" + force: "{{ force }}" + remaining_days: "{{ remaining_days }}" + terms_agreed: "{{ terms_agreed }}" + account_email: "{{ account_email }}" + register: challenge_data +- name: ({{ certgen_title }}) Print challenge data + debug: + var: challenge_data +- name: ({{ certgen_title }}) Create HTTP challenges + uri: + url: "http://{{ acme_host }}:5000/http/{{ item.key }}/{{ item.value['http-01'].resource[('.well-known/acme-challenge/'|length):] }}" + method: PUT + body_format: raw + body: "{{ item.value['http-01'].resource_value }}" + headers: + content-type: "application/octet-stream" + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'http-01'" +- name: ({{ certgen_title }}) Create DNS challenges + uri: + url: "http://{{ acme_host }}:5000/dns/{{ item.key }}" + method: PUT + body_format: json + body: "{{ item.value }}" + with_dict: "{{ challenge_data.challenge_data_dns }}" + when: "challenge_data is changed and challenge == 'dns-01'" +- name: ({{ certgen_title }}) Create TLS ALPN challenges (acme_challenge_cert_helper) + acme_challenge_cert_helper: + challenge: tls-alpn-01 + challenge_data: "{{ item.value['tls-alpn-01'] }}" + private_key_src: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" + private_key_passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}" + with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper') else {} }}" + register: tls_alpn_challenges + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Read private key + slurp: + src: '{{ remote_tmp_dir }}/{{ certificate_name }}.key' + register: slurp + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Set TLS ALPN challenges (acme_challenge_cert_helper) + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.domain }}/{{ item.identifier }}/certificate-and-key" + method: PUT + body_format: raw + body: "{{ item.challenge_certificate }}\n{{ slurp.content | b64decode }}" + headers: + content-type: "application/pem-certificate-chain" + with_items: "{{ tls_alpn_challenges.results if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper') else [] }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Create TLS ALPN challenges (der-value-b64) + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}/{{ item.value['tls-alpn-01'].resource_original }}/der-value-b64" + method: PUT + body_format: raw + body: "{{ item.value['tls-alpn-01'].resource_value }}" + headers: + content-type: "application/octet-stream" + with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'der-value-b64') else {} }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'der-value-b64')" +## ACME STEP 2 ################################################################################ +- name: ({{ certgen_title }}) Obtain cert, step 2 + acme_certificate: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}" + account_key_content: "{{ account_key_content | default(omit) }}" + account_key_passphrase: "{{ account_key_passphrase | default(omit) | default(omit, true) }}" + account_uri: "{{ challenge_data.account_uri }}" + modify_account: "{{ modify_account }}" + csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}" + csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}" + dest: "{{ remote_tmp_dir }}/{{ certificate_name }}.pem" + fullchain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-fullchain.pem" + chain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-chain.pem" + challenge: "{{ challenge }}" + deactivate_authzs: "{{ deactivate_authzs }}" + force: "{{ force }}" + remaining_days: "{{ remaining_days }}" + terms_agreed: "{{ terms_agreed }}" + account_email: "{{ account_email }}" + data: "{{ challenge_data }}" + retrieve_all_alternates: "{{ retrieve_all_alternates | default(omit) }}" + select_chain: "{{ select_chain | default(omit) if select_crypto_backend == 'cryptography' else omit }}" + register: certificate_obtain_result + when: challenge_data is changed +- name: ({{ certgen_title }}) Deleting HTTP challenges + uri: + url: "http://{{ acme_host }}:5000/http/{{ item.key }}/{{ item.value['http-01'].resource[('.well-known/acme-challenge/'|length):] }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'http-01'" +- name: ({{ certgen_title }}) Deleting DNS challenges + uri: + url: "http://{{ acme_host }}:5000/dns/{{ item.key }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data_dns }}" + when: "challenge_data is changed and challenge == 'dns-01'" +- name: ({{ certgen_title }}) Deleting TLS ALPN challenges + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01'" +- name: ({{ certgen_title }}) Get root certificate + get_url: + url: "http://{{ acme_host }}:5000/root-certificate-for-ca/{{ acme_expected_root_number | default(0) if select_crypto_backend == 'cryptography' else 0 }}" + dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-root.pem" +############################################################################################### diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/tests/validate.yml new file mode 100644 index 000000000..61947bf4e --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate/tests/validate.yml @@ -0,0 +1,202 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- 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: Read certificate 1 files + slurp: + src: '{{ remote_tmp_dir }}/{{ item }}' + loop: + - cert-1.pem + - cert-1-chain.pem + - cert-1-fullchain.pem + register: slurp +- 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]" + - "(slurp.results[0].content | b64decode) == cert_1_obtain_results.all_chains[cert_1_alternate | int].cert" + - "(slurp.results[1].content | b64decode) == cert_1_obtain_results.all_chains[cert_1_alternate | int].chain" + - "(slurp.results[2].content | b64decode) == 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: Read certificate 2 files + slurp: + src: '{{ remote_tmp_dir }}/{{ item }}' + loop: + - cert-2.pem + - cert-2-chain.pem + - cert-2-fullchain.pem + register: slurp +- 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]" + - "(slurp.results[0].content | b64decode) == cert_2_obtain_results.all_chains[cert_2_alternate | int].cert" + - "(slurp.results[1].content | b64decode) == cert_2_obtain_results.all_chains[cert_2_alternate | int].chain" + - "(slurp.results[2].content | b64decode) == 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: Read certificate 3 files + slurp: + src: '{{ remote_tmp_dir }}/{{ item }}' + loop: + - cert-3.pem + - cert-3-chain.pem + - cert-3-fullchain.pem + register: slurp +- 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]" + - "(slurp.results[0].content | b64decode) == cert_3_obtain_results.all_chains[cert_3_alternate | int].cert" + - "(slurp.results[1].content | b64decode) == cert_3_obtain_results.all_chains[cert_3_alternate | int].chain" + - "(slurp.results[2].content | b64decode) == 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 + +- block: + - 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" + when: acme_intermediates[0].subject_key_identifier is defined + +- block: + - name: Check that certificate 7 is valid + assert: + that: + - cert_7_valid is not failed + - name: Check that certificate 7 contains correct SANs + assert: + that: + - "'IP Address:127.0.0.1' in cert_8_text.stdout or 'IP:127.0.0.1' in cert_8_text.stdout" + when: acme_roots[2].subject_key_identifier is defined + +- block: + - name: Check that certificate 8 is valid + assert: + that: + - cert_8_valid is not failed + - name: Check that certificate 8 contains correct SANs + assert: + that: + - "'IP Address:127.0.0.1' in cert_8_text.stdout or 'IP:127.0.0.1' in cert_8_text.stdout" + when: cryptography_version.stdout is version('1.3', '>=') + +- 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' not in account_orders_urls" + - "'order_uris' in account_orders_urls" + - "account_orders_urls.order_uris[0] is string" + +- name: Validate that orders were retrieved as list of URLs (2/2) + assert: + that: + - "'account' in account_orders_urls2" + - "'orders' not in account_orders_urls2" + - "'order_uris' in account_orders_urls2" + - "account_orders_urls2.order_uris[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" + - "'order_uris' in account_orders_full" + - "account_orders_full.order_uris[0] 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" + - "'order_uris' in account_orders_full2" + - "account_orders_full2.order_uris[0] is string" diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/aliases b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/aliases new file mode 100644 index 000000000..b7f6d4f48 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/aliases @@ -0,0 +1,10 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/1 +azp/posix/1 +cloud/acme + +# For some reason connecting to helper containers does not work on the Alpine VMs +skip/alpine diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/meta/main.yml new file mode 100644 index 000000000..2e8ad10b8 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_acme + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/tasks/impl.yml new file mode 100644 index 000000000..c04d7d01e --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/tasks/impl.yml @@ -0,0 +1,118 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +## SET UP ACCOUNT KEYS ######################################################################## +- block: + - name: Generate account keys + openssl_privatekey: + path: "{{ remote_tmp_dir }}/{{ item.name }}.pem" + type: "{{ item.type }}" + size: "{{ item.size | default(omit) }}" + curve: "{{ item.curve | default(omit) }}" + force: true + loop: "{{ account_keys }}" + + vars: + account_keys: + - name: account-ec256 + type: ECC + curve: secp256r1 + - name: account-ec384 + type: ECC + curve: secp384r1 + - name: account-rsa + type: RSA + size: "{{ default_rsa_key_size }}" +## CREATE ACCOUNTS AND OBTAIN CERTIFICATES #################################################### +- name: Read account key (EC256) + slurp: + src: '{{ remote_tmp_dir }}/account-ec256.pem' + register: slurp_account_key +- 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: "{{ default_rsa_key_size }}" + subject_alt_name: "DNS:example.com" + subject_alt_name_critical: false + account_key_content: "{{ slurp_account_key.content | b64decode }}" + challenge: http-01 + modify_account: true + deactivate_authzs: false + force: false + remaining_days: 10 + terms_agreed: true + 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 + certificate_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else '' }}" + key_type: ec256 + subject_alt_name: "DNS:*.example.com" + subject_alt_name_critical: true + account_key: account-ec384 + challenge: dns-01 + modify_account: true + deactivate_authzs: true + force: false + remaining_days: 10 + terms_agreed: true + 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: false + account_key: account-rsa + challenge: dns-01 + modify_account: true + deactivate_authzs: false + force: false + remaining_days: 10 + terms_agreed: true + 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: "{{ remote_tmp_dir }}/account-ec256.pem" + certificate: "{{ remote_tmp_dir }}/cert-1.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + ignore_errors: true + register: cert_1_revoke +- name: Revoke certificate 2 via certificate private key + acme_certificate_revoke: + select_crypto_backend: "{{ select_crypto_backend }}" + private_key_src: "{{ remote_tmp_dir }}/cert-2.key" + private_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}" + certificate: "{{ remote_tmp_dir }}/cert-2.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + ignore_errors: true + register: cert_2_revoke +- name: Read account key (RSA) + slurp: + src: '{{ remote_tmp_dir }}/account-rsa.pem' + register: slurp_account_key +- name: Revoke certificate 3 via account key (fullchain) + acme_certificate_revoke: + select_crypto_backend: "{{ select_crypto_backend }}" + account_key_content: "{{ slurp_account_key.content | b64decode }}" + certificate: "{{ remote_tmp_dir }}/cert-3-fullchain.pem" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + ignore_errors: true + register: cert_3_revoke diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/tasks/main.yml new file mode 100644 index 000000000..68d47973d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/tasks/main.yml @@ -0,0 +1,40 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- 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: "{{ remote_tmp_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ remote_tmp_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', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/tasks/obtain-cert.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/tasks/obtain-cert.yml new file mode 100644 index 000000000..6882e5339 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/tasks/obtain-cert.yml @@ -0,0 +1,159 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +## PRIVATE KEY ################################################################################ +- name: ({{ certgen_title }}) Create cert private key + openssl_privatekey: + path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" + type: "{{ 'RSA' if key_type == 'rsa' else 'ECC' }}" + size: "{{ rsa_bits if key_type == 'rsa' else omit }}" + curve: >- + {{ omit if key_type == 'rsa' else + 'secp256r1' if key_type == 'ec256' else + 'secp384r1' if key_type == 'ec384' else + 'secp521r1' if key_type == 'ec521' else + 'invalid value for key_type!' }} + passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}" + cipher: "{{ 'auto' if certificate_passphrase | default() else omit }}" + force: true +## CSR ######################################################################################## +- name: ({{ certgen_title }}) Create cert CSR + openssl_csr: + path: "{{ remote_tmp_dir }}/{{ certificate_name }}.csr" + privatekey_path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" + privatekey_passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}" + subject_alt_name: "{{ subject_alt_name }}" + subject_alt_name_critical: "{{ subject_alt_name_critical }}" + return_content: true + register: csr_result +## ACME STEP 1 ################################################################################ +- name: ({{ certgen_title }}) Obtain cert, step 1 + acme_certificate: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}" + account_key_content: "{{ account_key_content | default(omit) }}" + account_key_passphrase: "{{ account_key_passphrase | default(omit) | default(omit, true) }}" + modify_account: "{{ modify_account }}" + csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}" + csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}" + dest: "{{ remote_tmp_dir }}/{{ certificate_name }}.pem" + fullchain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-fullchain.pem" + chain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-chain.pem" + challenge: "{{ challenge }}" + deactivate_authzs: "{{ deactivate_authzs }}" + force: "{{ force }}" + remaining_days: "{{ remaining_days }}" + terms_agreed: "{{ terms_agreed }}" + account_email: "{{ account_email }}" + register: challenge_data +- name: ({{ certgen_title }}) Print challenge data + debug: + var: challenge_data +- name: ({{ certgen_title }}) Create HTTP challenges + uri: + url: "http://{{ acme_host }}:5000/http/{{ item.key }}/{{ item.value['http-01'].resource[('.well-known/acme-challenge/'|length):] }}" + method: PUT + body_format: raw + body: "{{ item.value['http-01'].resource_value }}" + headers: + content-type: "application/octet-stream" + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'http-01'" +- name: ({{ certgen_title }}) Create DNS challenges + uri: + url: "http://{{ acme_host }}:5000/dns/{{ item.key }}" + method: PUT + body_format: json + body: "{{ item.value }}" + with_dict: "{{ challenge_data.challenge_data_dns }}" + when: "challenge_data is changed and challenge == 'dns-01'" +- name: ({{ certgen_title }}) Create TLS ALPN challenges (acme_challenge_cert_helper) + acme_challenge_cert_helper: + challenge: tls-alpn-01 + challenge_data: "{{ item.value['tls-alpn-01'] }}" + private_key_src: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" + private_key_passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}" + with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper') else {} }}" + register: tls_alpn_challenges + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Read private key + slurp: + src: '{{ remote_tmp_dir }}/{{ certificate_name }}.key' + register: slurp + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Set TLS ALPN challenges (acme_challenge_cert_helper) + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.domain }}/{{ item.identifier }}/certificate-and-key" + method: PUT + body_format: raw + body: "{{ item.challenge_certificate }}\n{{ slurp.content | b64decode }}" + headers: + content-type: "application/pem-certificate-chain" + with_items: "{{ tls_alpn_challenges.results if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper') else [] }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Create TLS ALPN challenges (der-value-b64) + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}/{{ item.value['tls-alpn-01'].resource_original }}/der-value-b64" + method: PUT + body_format: raw + body: "{{ item.value['tls-alpn-01'].resource_value }}" + headers: + content-type: "application/octet-stream" + with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'der-value-b64') else {} }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'der-value-b64')" +## ACME STEP 2 ################################################################################ +- name: ({{ certgen_title }}) Obtain cert, step 2 + acme_certificate: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}" + account_key_content: "{{ account_key_content | default(omit) }}" + account_key_passphrase: "{{ account_key_passphrase | default(omit) | default(omit, true) }}" + account_uri: "{{ challenge_data.account_uri }}" + modify_account: "{{ modify_account }}" + csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}" + csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}" + dest: "{{ remote_tmp_dir }}/{{ certificate_name }}.pem" + fullchain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-fullchain.pem" + chain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-chain.pem" + challenge: "{{ challenge }}" + deactivate_authzs: "{{ deactivate_authzs }}" + force: "{{ force }}" + remaining_days: "{{ remaining_days }}" + terms_agreed: "{{ terms_agreed }}" + account_email: "{{ account_email }}" + data: "{{ challenge_data }}" + retrieve_all_alternates: "{{ retrieve_all_alternates | default(omit) }}" + select_chain: "{{ select_chain | default(omit) if select_crypto_backend == 'cryptography' else omit }}" + register: certificate_obtain_result + when: challenge_data is changed +- name: ({{ certgen_title }}) Deleting HTTP challenges + uri: + url: "http://{{ acme_host }}:5000/http/{{ item.key }}/{{ item.value['http-01'].resource[('.well-known/acme-challenge/'|length):] }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'http-01'" +- name: ({{ certgen_title }}) Deleting DNS challenges + uri: + url: "http://{{ acme_host }}:5000/dns/{{ item.key }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data_dns }}" + when: "challenge_data is changed and challenge == 'dns-01'" +- name: ({{ certgen_title }}) Deleting TLS ALPN challenges + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01'" +- name: ({{ certgen_title }}) Get root certificate + get_url: + url: "http://{{ acme_host }}:5000/root-certificate-for-ca/{{ acme_expected_root_number | default(0) if select_crypto_backend == 'cryptography' else 0 }}" + dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-root.pem" +############################################################################################### diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/tests/validate.yml new file mode 100644 index 000000000..4c06fc56e --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_revoke/tests/validate.yml @@ -0,0 +1,20 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- 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 diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_challenge_cert_helper/aliases b/ansible_collections/community/crypto/tests/integration/targets/acme_challenge_cert_helper/aliases new file mode 100644 index 000000000..b7f6d4f48 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_challenge_cert_helper/aliases @@ -0,0 +1,10 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/1 +azp/posix/1 +cloud/acme + +# For some reason connecting to helper containers does not work on the Alpine VMs +skip/alpine diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_challenge_cert_helper/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_challenge_cert_helper/meta/main.yml new file mode 100644 index 000000000..2e8ad10b8 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_challenge_cert_helper/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_acme + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_challenge_cert_helper/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_challenge_cert_helper/tasks/main.yml new file mode 100644 index 000000000..ef40ec601 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_challenge_cert_helper/tasks/main.yml @@ -0,0 +1,38 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- block: + - name: Generate ECC256 accoun keys + openssl_privatekey: + path: "{{ remote_tmp_dir }}/account-ec256.pem" + type: ECC + curve: secp256r1 + force: true + - 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: "{{ default_rsa_key_size }}" + subject_alt_name: "DNS:example.com" + subject_alt_name_critical: false + account_key: account-ec256 + challenge: tls-alpn-01 + challenge_alpn_tls: acme_challenge_cert_helper + modify_account: true + deactivate_authzs: false + force: false + remaining_days: 10 + terms_agreed: true + account_email: "example@example.org" + + when: cryptography_version.stdout is version('1.5', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_challenge_cert_helper/tasks/obtain-cert.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_challenge_cert_helper/tasks/obtain-cert.yml new file mode 100644 index 000000000..6882e5339 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_challenge_cert_helper/tasks/obtain-cert.yml @@ -0,0 +1,159 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +## PRIVATE KEY ################################################################################ +- name: ({{ certgen_title }}) Create cert private key + openssl_privatekey: + path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" + type: "{{ 'RSA' if key_type == 'rsa' else 'ECC' }}" + size: "{{ rsa_bits if key_type == 'rsa' else omit }}" + curve: >- + {{ omit if key_type == 'rsa' else + 'secp256r1' if key_type == 'ec256' else + 'secp384r1' if key_type == 'ec384' else + 'secp521r1' if key_type == 'ec521' else + 'invalid value for key_type!' }} + passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}" + cipher: "{{ 'auto' if certificate_passphrase | default() else omit }}" + force: true +## CSR ######################################################################################## +- name: ({{ certgen_title }}) Create cert CSR + openssl_csr: + path: "{{ remote_tmp_dir }}/{{ certificate_name }}.csr" + privatekey_path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" + privatekey_passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}" + subject_alt_name: "{{ subject_alt_name }}" + subject_alt_name_critical: "{{ subject_alt_name_critical }}" + return_content: true + register: csr_result +## ACME STEP 1 ################################################################################ +- name: ({{ certgen_title }}) Obtain cert, step 1 + acme_certificate: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}" + account_key_content: "{{ account_key_content | default(omit) }}" + account_key_passphrase: "{{ account_key_passphrase | default(omit) | default(omit, true) }}" + modify_account: "{{ modify_account }}" + csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}" + csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}" + dest: "{{ remote_tmp_dir }}/{{ certificate_name }}.pem" + fullchain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-fullchain.pem" + chain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-chain.pem" + challenge: "{{ challenge }}" + deactivate_authzs: "{{ deactivate_authzs }}" + force: "{{ force }}" + remaining_days: "{{ remaining_days }}" + terms_agreed: "{{ terms_agreed }}" + account_email: "{{ account_email }}" + register: challenge_data +- name: ({{ certgen_title }}) Print challenge data + debug: + var: challenge_data +- name: ({{ certgen_title }}) Create HTTP challenges + uri: + url: "http://{{ acme_host }}:5000/http/{{ item.key }}/{{ item.value['http-01'].resource[('.well-known/acme-challenge/'|length):] }}" + method: PUT + body_format: raw + body: "{{ item.value['http-01'].resource_value }}" + headers: + content-type: "application/octet-stream" + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'http-01'" +- name: ({{ certgen_title }}) Create DNS challenges + uri: + url: "http://{{ acme_host }}:5000/dns/{{ item.key }}" + method: PUT + body_format: json + body: "{{ item.value }}" + with_dict: "{{ challenge_data.challenge_data_dns }}" + when: "challenge_data is changed and challenge == 'dns-01'" +- name: ({{ certgen_title }}) Create TLS ALPN challenges (acme_challenge_cert_helper) + acme_challenge_cert_helper: + challenge: tls-alpn-01 + challenge_data: "{{ item.value['tls-alpn-01'] }}" + private_key_src: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" + private_key_passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}" + with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper') else {} }}" + register: tls_alpn_challenges + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Read private key + slurp: + src: '{{ remote_tmp_dir }}/{{ certificate_name }}.key' + register: slurp + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Set TLS ALPN challenges (acme_challenge_cert_helper) + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.domain }}/{{ item.identifier }}/certificate-and-key" + method: PUT + body_format: raw + body: "{{ item.challenge_certificate }}\n{{ slurp.content | b64decode }}" + headers: + content-type: "application/pem-certificate-chain" + with_items: "{{ tls_alpn_challenges.results if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper') else [] }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Create TLS ALPN challenges (der-value-b64) + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}/{{ item.value['tls-alpn-01'].resource_original }}/der-value-b64" + method: PUT + body_format: raw + body: "{{ item.value['tls-alpn-01'].resource_value }}" + headers: + content-type: "application/octet-stream" + with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'der-value-b64') else {} }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'der-value-b64')" +## ACME STEP 2 ################################################################################ +- name: ({{ certgen_title }}) Obtain cert, step 2 + acme_certificate: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}" + account_key_content: "{{ account_key_content | default(omit) }}" + account_key_passphrase: "{{ account_key_passphrase | default(omit) | default(omit, true) }}" + account_uri: "{{ challenge_data.account_uri }}" + modify_account: "{{ modify_account }}" + csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}" + csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}" + dest: "{{ remote_tmp_dir }}/{{ certificate_name }}.pem" + fullchain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-fullchain.pem" + chain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-chain.pem" + challenge: "{{ challenge }}" + deactivate_authzs: "{{ deactivate_authzs }}" + force: "{{ force }}" + remaining_days: "{{ remaining_days }}" + terms_agreed: "{{ terms_agreed }}" + account_email: "{{ account_email }}" + data: "{{ challenge_data }}" + retrieve_all_alternates: "{{ retrieve_all_alternates | default(omit) }}" + select_chain: "{{ select_chain | default(omit) if select_crypto_backend == 'cryptography' else omit }}" + register: certificate_obtain_result + when: challenge_data is changed +- name: ({{ certgen_title }}) Deleting HTTP challenges + uri: + url: "http://{{ acme_host }}:5000/http/{{ item.key }}/{{ item.value['http-01'].resource[('.well-known/acme-challenge/'|length):] }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'http-01'" +- name: ({{ certgen_title }}) Deleting DNS challenges + uri: + url: "http://{{ acme_host }}:5000/dns/{{ item.key }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data_dns }}" + when: "challenge_data is changed and challenge == 'dns-01'" +- name: ({{ certgen_title }}) Deleting TLS ALPN challenges + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01'" +- name: ({{ certgen_title }}) Get root certificate + get_url: + url: "http://{{ acme_host }}:5000/root-certificate-for-ca/{{ acme_expected_root_number | default(0) if select_crypto_backend == 'cryptography' else 0 }}" + dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-root.pem" +############################################################################################### diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/aliases b/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/aliases new file mode 100644 index 000000000..b7f6d4f48 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/aliases @@ -0,0 +1,10 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/1 +azp/posix/1 +cloud/acme + +# For some reason connecting to helper containers does not work on the Alpine VMs +skip/alpine diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/meta/main.yml new file mode 100644 index 000000000..84b7f3f97 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_acme + - setup_remote_tmp_dir + - prepare_jinja2_compat diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tasks/impl.yml new file mode 100644 index 000000000..4eed1031a --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tasks/impl.yml @@ -0,0 +1,168 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- block: + - name: Generate account keys + openssl_privatekey: + path: "{{ remote_tmp_dir }}/{{ item }}.pem" + type: ECC + curve: secp256r1 + force: true + loop: "{{ account_keys }}" + + - name: Parse account keys (to ease debugging some test failures) + openssl_privatekey_info: + path: "{{ remote_tmp_dir }}/{{ item }}.pem" + return_private_key_data: true + loop: "{{ account_keys }}" + + vars: + account_keys: + - accountkey + +- name: Get directory + acme_inspect: + acme_directory: https://{{ acme_host }}:14000/dir + acme_version: 2 + validate_certs: false + 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: false + account_key_src: "{{ remote_tmp_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: false + account_key_src: "{{ remote_tmp_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: false + account_key_src: "{{ remote_tmp_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: false + account_key_src: "{{ remote_tmp_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: false + account_key_src: "{{ remote_tmp_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: false + account_key_src: "{{ remote_tmp_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: false + account_key_src: "{{ remote_tmp_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: false + account_key_src: "{{ remote_tmp_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: false + account_key_src: "{{ remote_tmp_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 not in ['pending', 'processing']" + retries: 20 + delay: 1 +- debug: var=validation_result diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tasks/main.yml new file mode 100644 index 000000000..68d47973d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tasks/main.yml @@ -0,0 +1,40 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- 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: "{{ remote_tmp_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ remote_tmp_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', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tests/validate.yml new file mode 100644 index 000000000..53dfa928c --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tests/validate.yml @@ -0,0 +1,135 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- 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 in ['pending', 'processing'] + - 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 }}" diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/aliases b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/aliases new file mode 100644 index 000000000..857789143 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert1-chain.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert1-chain.pem new file mode 100644 index 000000000..3e0f6c085 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert1-chain.pem @@ -0,0 +1,22 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert1-fullchain.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert1-fullchain.pem new file mode 100644 index 000000000..e12a7ca81 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert1-fullchain.pem @@ -0,0 +1,51 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert1-root.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert1-root.pem new file mode 100644 index 000000000..546c95e30 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert1-root.pem @@ -0,0 +1,16 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert1.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert1.pem new file mode 100644 index 000000000..d00d252d8 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert1.pem @@ -0,0 +1,29 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-altchain.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-altchain.pem new file mode 100644 index 000000000..4e82cb56d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-altchain.pem @@ -0,0 +1,32 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-altroot.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-altroot.pem new file mode 100644 index 000000000..b85c8037f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-altroot.pem @@ -0,0 +1,31 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-chain.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-chain.pem new file mode 100644 index 000000000..0002462ce --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-chain.pem @@ -0,0 +1,27 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-fullchain.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-fullchain.pem new file mode 100644 index 000000000..cf75a331c --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-fullchain.pem @@ -0,0 +1,72 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-root.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-root.pem new file mode 100644 index 000000000..b2e43c938 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2-root.pem @@ -0,0 +1,20 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2.pem new file mode 100644 index 000000000..834eedc44 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/cert2.pem @@ -0,0 +1,45 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots.pem new file mode 100644 index 000000000..ee6c058d3 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots.pem @@ -0,0 +1,3733 @@ +# ACCVRAIZ1 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# AC RAIZ FNMT-RCM +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +# Actalis Authentication Root CA +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# AddTrust External Root +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- + +# AffirmTrust Commercial +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +# AffirmTrust Networking +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +# AffirmTrust Premium +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- + +# AffirmTrust Premium ECC +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- + +# Amazon Root CA 1 +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +# Amazon Root CA 2 +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- + +# Amazon Root CA 3 +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +# Amazon Root CA 4 +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +# Atos TrustedRoot 2011 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +# Autoridad de Certificacion Firmaprofesional CIF A62634068 +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy +MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD +VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv +ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl +AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF +661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9 +am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1 +ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481 +PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS +3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k +SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF +3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM +ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g +StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz +Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB +jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +# Baltimore CyberTrust Root +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +# Buypass Class 2 Root CA +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Buypass Class 3 Root CA +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# CA Disig Root R2 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# Certigna +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +# Certinomis - Root CA +-----BEGIN CERTIFICATE----- +MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjET +MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAb +BgNVBAMTFENlcnRpbm9taXMgLSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMz +MTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMx +FzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRDZXJ0aW5vbWlzIC0g +Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQosP5L2 +fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJfl +LieY6pOod5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQV +WZUKxkd8aRi5pwP5ynapz8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDF +TKWrteoB4owuZH9kb/2jJZOLyKIOSY008B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb +5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09xRLWtwHkziOC/7aOgFLSc +CbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE6OXWk6Ri +wsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJ +wx3tFvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SG +m/lg0h9tkQPTYKbVPZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4 +F2iw4lNVYC2vPsKD2NkJK/DAZNuHi5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZng +WVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I6tNxIqSSaHh0 +2TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF +AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/ +0KGRHCwPT5iVWVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWw +F6YSjNRieOpWauwK0kDDPAUwPk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZS +g081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAXlCOotQqSD7J6wWAsOMwaplv/8gzj +qh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJy29SWwNyhlCVCNSN +h4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9Iff/ +ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8V +btaw5BngDwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwj +Y/M50n92Uaf0yKHxDHYiI0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ +8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nMcyrDflOR1m749fPH0FFNjkulW+YZFzvW +gQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVrhkIGuUE= +-----END CERTIFICATE----- + +# Certplus Class 2 Primary CA +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw +PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz +cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9 +MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz +IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ +ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR +VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL +kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd +EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas +H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0 +HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud +DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4 +QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu +Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/ +AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8 +yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR +FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA +ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB +kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 +l7+ijrRU +-----END CERTIFICATE----- + +# Certplus Root CA G1 +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgISESBVg+QtPlRWhS2DN7cs3EYRMA0GCSqGSIb3DQEBDQUA +MD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy +dHBsdXMgUm9vdCBDQSBHMTAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBa +MD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy +dHBsdXMgUm9vdCBDQSBHMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ANpQh7bauKk+nWT6VjOaVj0W5QOVsjQcmm1iBdTYj+eJZJ+622SLZOZ5KmHNr49a +iZFluVj8tANfkT8tEBXgfs+8/H9DZ6itXjYj2JizTfNDnjl8KvzsiNWI7nC9hRYt +6kuJPKNxQv4c/dMcLRC4hlTqQ7jbxofaqK6AJc96Jh2qkbBIb6613p7Y1/oA/caP +0FG7Yn2ksYyy/yARujVjBYZHYEMzkPZHogNPlk2dT8Hq6pyi/jQu3rfKG3akt62f +6ajUeD94/vI4CTYd0hYCyOwqaK/1jpTvLRN6HkJKHRUxrgwEV/xhc/MxVoYxgKDE +EW4wduOU8F8ExKyHcomYxZ3MVwia9Az8fXoFOvpHgDm2z4QTd28n6v+WZxcIbekN +1iNQMLAVdBM+5S//Ds3EC0pd8NgAM0lm66EYfFkuPSi5YXHLtaW6uOrc4nBvCGrc +h2c0798wct3zyT8j/zXhviEpIDCB5BmlIOklynMxdCm+4kLV87ImZsdo/Rmz5yCT +mehd4F6H50boJZwKKSTUzViGUkAksnsPmBIgJPaQbEfIDbsYIC7Z/fyL8inqh3SV +4EJQeIQEQWGw9CEjjy3LKCHyamz0GqbFFLQ3ZU+V/YDI+HLlJWvEYLF7bY5KinPO +WftwenMGE9nTdDckQQoRb5fc5+R+ob0V8rqHDz1oihYHAgMBAAGjYzBhMA4GA1Ud +DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSowcCbkahDFXxd +Bie0KlHYlwuBsTAfBgNVHSMEGDAWgBSowcCbkahDFXxdBie0KlHYlwuBsTANBgkq +hkiG9w0BAQ0FAAOCAgEAnFZvAX7RvUz1isbwJh/k4DgYzDLDKTudQSk0YcbX8ACh +66Ryj5QXvBMsdbRX7gp8CXrc1cqh0DQT+Hern+X+2B50ioUHj3/MeXrKls3N/U/7 +/SMNkPX0XtPGYX2eEeAC7gkE2Qfdpoq3DIMku4NQkv5gdRE+2J2winq14J2by5BS +S7CTKtQ+FjPlnsZlFT5kOwQ/2wyPX1wdaR+v8+khjPPvl/aatxm2hHSco1S1cE5j +2FddUyGbQJJD+tZ3VTNPZNX70Cxqjm0lpu+F6ALEUz65noe8zDUa3qHpimOHZR4R +Kttjd5cUvpoUmRGywO6wT/gUITJDT5+rosuoD6o7BlXGEilXCNQ314cnrUlZp5Gr +RHpejXDbl85IULFzk/bwg2D5zfHhMf1bfHEhYxQUqq/F3pN+aLHsIqKqkHWetUNy +6mSjhEv9DKgma3GX7lZjZuhCVPnHHd/Qj1vfyDBviP4NxDMcU6ij/UgQ8uQKTuEV +V/xuZDDCVRHc6qnNSlSsKWNEz0pAoNZoWRsz+e86i9sgktxChL8Bq4fA1SCC28a5 +g4VCXA9DO2pJNdWY9BW/+mGBDAkgGNLQFwzLSABQ6XaCjGTXOqAHVcweMcDvOrRl +++O/QmueD6i9a5jc2NvLi6Td11n0bt3+qsOR0C5CB8AMTVPNJLFMWx5R9N/pkvo= +-----END CERTIFICATE----- + +# Certplus Root CA G2 +-----BEGIN CERTIFICATE----- +MIICHDCCAaKgAwIBAgISESDZkc6uo+jF5//pAq/Pc7xVMAoGCCqGSM49BAMDMD4x +CzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs +dXMgUm9vdCBDQSBHMjAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBaMD4x +CzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs +dXMgUm9vdCBDQSBHMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABM0PW1aC3/BFGtat +93nwHcmsltaeTpwftEIRyoa/bfuFo8XlGVzX7qY/aWfYeOKmycTbLXku54uNAm8x +Ik0G42ByRZ0OQneezs/lf4WbGOT8zC5y0xaTTsqZY1yhBSpsBqNjMGEwDgYDVR0P +AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNqDYwJ5jtpMxjwj +FNiPwyCrKGBZMB8GA1UdIwQYMBaAFNqDYwJ5jtpMxjwjFNiPwyCrKGBZMAoGCCqG +SM49BAMDA2gAMGUCMHD+sAvZ94OX7PNVHdTcswYO/jOYnYs5kGuUIe22113WTNch +p+e/IQ8rzfcq3IUHnQIxAIYUFuXcsGXCwI4Un78kFmjlvPl5adytRSv3tjFzzAal +U5ORGpOucGpnutee5WEaXw== +-----END CERTIFICATE----- + +# certSIGN ROOT CA +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +# Certum Trusted Network CA 2 +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- + +# Certum Trusted Network CA +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# CFCA EV ROOT +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +# Chambers of Commerce Root - 2008 +-----BEGIN CERTIFICATE----- +MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz +IG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz +MTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj +dXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw +EAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp +MCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9 +28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq +VKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q +DuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR +5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL +ZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a +Sd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl +UlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s ++12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5 +Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj +ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx +hduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV +HQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1 ++HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN +YWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t +L2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy +ZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt +IDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV +HSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w +DQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW +PJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF +5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1 +glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH +FoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2 +pSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD +xvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG +tjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq +jktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De +fhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg +OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ +d0jQ +-----END CERTIFICATE----- + +# Comodo AAA Services root +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +# COMODO Certification Authority +-----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----- + +# COMODO ECC Certification Authority +-----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----- + +# COMODO RSA Certification Authority +-----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----- + +# Cybertrust Global Root +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG +A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh +bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE +ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 +7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS +J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y +HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP +t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz +FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY +XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw +hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js +MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA +A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj +Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx +XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o +omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc +A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +# Deutsche Telekom Root CA 2 +-----BEGIN CERTIFICATE----- +MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc +MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj +IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB +IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE +RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl +U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290 +IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU +ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC +QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr +rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S +NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc +QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH +txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP +BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC +AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp +tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa +IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl +6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+ +xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU +Cm26OWMohpLzGITY+9HPBVZkVw== +-----END CERTIFICATE----- + +# DigiCert Assured ID Root CA +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +# DigiCert Assured ID Root G2 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# DigiCert Assured ID Root G3 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# DigiCert Global Root CA +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +# DigiCert Global Root G2 +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# DigiCert Global Root G3 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# DigiCert High Assurance EV Root CA +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +# DigiCert Trusted Root G4 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# DST Root CA X3 +-----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----- + +# D-TRUST Root Class 3 CA 2 2009 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# D-TRUST Root Class 3 CA 2 EV 2009 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# EC-ACC +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB +8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy +dGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1 +YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3 +dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh +IEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD +LUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG +EwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g +KE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD +ZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu +bmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg +ZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R +85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm +4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV +HMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd +QlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t +lGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB +o4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4 +opvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo +dHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW +ZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN +AQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y +/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k +SBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy +Rp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS +Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl +nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI= +-----END CERTIFICATE----- + +# EE Certification Centre Root CA +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG +CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy +MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl +ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS +b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy +euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO +bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw +WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d +MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE +1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/ +zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB +BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF +BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV +v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG +E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u +uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW +iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v +GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0= +-----END CERTIFICATE----- + +# Entrust.net Premium 2048 Secure Server CA +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 +MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub +j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo +U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b +u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ +bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er +fF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +# Entrust Root Certification Authority +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +# Entrust Root Certification Authority - EC1 +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +# Entrust Root Certification Authority - G2 +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- + +# ePKI Root Certification Authority +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +# E-Tugra Certification Authority +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV +BAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC +aWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV +BAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1 +Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz +MDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+ +BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp +em1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY +B4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH +D5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF +Q9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo +q1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D +k14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH +fC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut +dEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM +ti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8 +zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX +U8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6 +Jyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5 +XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF +Nzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR +HTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY +GwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c +77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3 ++GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK +vJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6 +FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl +yb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P +AJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD +y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d +NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- + +# GDCA TrustAUTH R5 ROOT +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +# GeoTrust Global CA +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- + +# GeoTrust Primary Certification Authority +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo +R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx +MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 +AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA +ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 +7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W +kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI +mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ +KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 +6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl +4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K +oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj +UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU +AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +# GeoTrust Primary Certification Authority - G2 +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL +MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj +KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2 +MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw +NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV +BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL +So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal +tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG +CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT +qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz +rD6ogRLQy7rQkgu2npaqBA+K +-----END CERTIFICATE----- + +# GeoTrust Primary Certification Authority - G3 +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT +MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ +BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 +BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz ++uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm +hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn +5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W +JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL +DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC +huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB +AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB +zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN +kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH +SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G +spki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +# GeoTrust Universal CA +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy +c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 +IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV +VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 +cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT +QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh +F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v +c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w +mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd +VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX +teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ +f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe +Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ +nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY +MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX +IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn +ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z +uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN +Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja +QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW +koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 +ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt +DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm +bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +# GeoTrust Universal CA 2 +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy +c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD +VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 +c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 +WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG +FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq +XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL +se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb +KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd +IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 +y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt +hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc +QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 +Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV +HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ +KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ +L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr +Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo +ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY +T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz +GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m +1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV +OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH +6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX +QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +# Global Chambersign Root - 2008 +-----BEGIN CERTIFICATE----- +MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD +aGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx +MjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy +cmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG +A1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl +BgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed +KYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7 +G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2 +zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4 +ddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG +HoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2 +Id3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V +yJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e +beksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r +6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh +wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog +zCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW +BBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr +ru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp +ZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk +cmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt +YSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC +CQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI +hvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ +UohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz +X1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x +fxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz +a2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd +Yhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd +SqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O +AP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso +M0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge +v8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z +09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B +-----END CERTIFICATE----- + +# GlobalSign ECC Root CA - R4 +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ +FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F +uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX +kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs +ewv4n4Q= +-----END CERTIFICATE----- + +# GlobalSign ECC Root CA - R5 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# GlobalSign Root CA +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +# GlobalSign Root CA - R2 +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +# GlobalSign Root CA - R3 +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Go Daddy Class 2 CA +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- + +# Go Daddy Root Certificate Authority - G2 +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Hellenic Academic and Research Institutions ECC RootCA 2015 +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +# Hellenic Academic and Research Institutions RootCA 2011 +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix +RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p +YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw +NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK +EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl +cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz +dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ +fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns +bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD +75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP +FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV +HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp +5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu +b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA +A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p +6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7 +dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys +Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI +l7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- + +# Hellenic Academic and Research Institutions RootCA 2015 +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +# Hongkong Post Root CA 1 +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx +FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg +Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG +A1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr +b25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ +jVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn +PzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh +ZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9 +nnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h +q5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED +MA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC +mEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3 +7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB +oiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs +EhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO +fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi +AmvZWg== +-----END CERTIFICATE----- + +# IdenTrust Commercial Root CA 1 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +# IdenTrust Public Sector Root CA 1 +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +# ISRG Root X1 +-----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----- + +# Izenpe.com +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +# LuxTrust Global Root 2 +-----BEGIN CERTIFICATE----- +MIIFwzCCA6ugAwIBAgIUCn6m30tEntpqJIWe5rgV0xZ/u7EwDQYJKoZIhvcNAQEL +BQAwRjELMAkGA1UEBhMCTFUxFjAUBgNVBAoMDUx1eFRydXN0IFMuQS4xHzAdBgNV +BAMMFkx1eFRydXN0IEdsb2JhbCBSb290IDIwHhcNMTUwMzA1MTMyMTU3WhcNMzUw +MzA1MTMyMTU3WjBGMQswCQYDVQQGEwJMVTEWMBQGA1UECgwNTHV4VHJ1c3QgUy5B +LjEfMB0GA1UEAwwWTHV4VHJ1c3QgR2xvYmFsIFJvb3QgMjCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANeFl78RmOnwYoNMPIf5U2o3C/IPPIfOb9wmKb3F +ibrJgz337spbxm1Jc7TJRqMbNBM/wYlFV/TZsfs2ZUv7COJIcRHIbjuend+JZTem +hfY7RBi2xjcwYkSSl2l9QjAk5A0MiWtj3sXh306pFGxT4GHO9hcvHTy95iJMHZP1 +EMShduxq3sVs35a0VkBCwGKSMKEtFZSg0iAGCW5qbeXrt77U8PEVfIvmTroTzEsn +Xpk8F12PgX8zPU/TPxvsXD/wPEx1bvKm1Z3aLQdjAsZy6ZS8TEmVT4hSyNvoaYL4 +zDRbIvCGp4m9SAptZoFtyMhk+wHh9OHe2Z7d21vUKpkmFRseTJIpgp7VkoGSQXAZ +96Tlk0u8d2cx3Rz9MXANF5kM+Qw5GSoXtTBxVdUPrljhPS80m8+f9niFwpN6cj5m +j5wWEWCPnolvZ77gR1o7DJpni89Gxq44o/KnvObWhWszJHAiS8sIm7vI+AIpHb4g +DEa/a4ebsypmQjVGbKq6rfmYe+lQVRQxv7HaLe2ArWgk+2mr2HETMOZns4dA/Yl+ +8kPREd8vZS9kzl8UubG/Mb2HeFpZZYiq/FkySIbWTLkpS5XTdvN3JW1CHDiDTf2j +X5t/Lax5Gw5CMZdjpPuKadUiDTSQMC6otOBttpSsvItO13D8xTiOZCXhTTmQzsmH +hFhxAgMBAAGjgagwgaUwDwYDVR0TAQH/BAUwAwEB/zBCBgNVHSAEOzA5MDcGByuB +KwEBAQowLDAqBggrBgEFBQcCARYeaHR0cHM6Ly9yZXBvc2l0b3J5Lmx1eHRydXN0 +Lmx1MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBT/GCh2+UgFLKGu8SsbK7JT ++Et8szAdBgNVHQ4EFgQU/xgodvlIBSyhrvErGyuyU/hLfLMwDQYJKoZIhvcNAQEL +BQADggIBAGoZFO1uecEsh9QNcH7X9njJCwROxLHOk3D+sFTAMs2ZMGQXvw/l4jP9 +BzZAcg4atmpZ1gDlaCDdLnINH2pkMSCEfUmmWjfrRcmF9dTHF5kH5ptV5AzoqbTO +jFu1EVzPig4N1qx3gf4ynCSecs5U89BvolbW7MM3LGVYvlcAGvI1+ut7MV3CwRI9 +loGIlonBWVx65n9wNOeD4rHh4bhY79SV5GCc8JaXcozrhAIuZY+kt9J/Z93I055c +qqmkoCUUBpvsT34tC38ddfEz2O3OuHVtPlu5mB0xDVbYQw8wkbIEa91WvpWAVWe+ +2M2D2RjuLg+GLZKecBPs3lHJQ3gCpU3I+V/EkVhGFndadKpAvAefMLmx9xIX3eP/ +JEAdemrRTxgKqpAd60Ae36EeRJIQmvKN4dFLRp7oRUKX6kWZ8+xm1QL68qZKJKre +zrnK+T+Tb/mjuuqlPpmt/f97mfVl7vBZKGfXkJWkE4SphMHozs51k2MavDzq1WQf +LSoSOcbDWjLtR5EWDrw4wVDej8oqkDQc7kGUnF4ZLvhFSZl0kbAEb+MEWrGrKqv+ +x9CWttrhSmQGbmBNvUJO/3jaJMobtNeWOWyu8Q6qp31IiyBMz2TWuJdGsE7RKlY6 +oJO9r4Ak4Ap+58rVyuiFVdw2KuGUaJPHZnJED4AhMmwlxyOAgwrr +-----END CERTIFICATE----- + +# Microsec e-Szigno Root CA 2009 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# NetLock Arany (Class Gold) Főtanúsítvány +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Network Solutions Certificate Authority +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi +MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV +UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO +ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz +c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP +OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl +mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF +BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4 +qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw +gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu +bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp +dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8 +6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/ +h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH +/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN +pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +# OISTE WISeKey Global Root GA CA +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB +ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly +aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w +NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G +A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX +SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR +VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 +w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF +mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg +4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 +4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw +EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx +SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 +ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 +vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi +Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ +/L7fCg0= +-----END CERTIFICATE----- + +# OISTE WISeKey Global Root GB CA +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +# OpenTrust Root CA G1 +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgISESCzkFU5fX82bWTCp59rY45nMA0GCSqGSIb3DQEBCwUA +MEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w +ZW5UcnVzdCBSb290IENBIEcxMB4XDTE0MDUyNjA4NDU1MFoXDTM4MDExNTAwMDAw +MFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU +T3BlblRydXN0IFJvb3QgQ0EgRzEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQD4eUbalsUwXopxAy1wpLuwxQjczeY1wICkES3d5oeuXT2R0odsN7faYp6b +wiTXj/HbpqbfRm9RpnHLPhsxZ2L3EVs0J9V5ToybWL0iEA1cJwzdMOWo010hOHQX +/uMftk87ay3bfWAfjH1MBcLrARYVmBSO0ZB3Ij/swjm4eTrwSSTilZHcYTSSjFR0 +77F9jAHiOH3BX2pfJLKOYheteSCtqx234LSWSE9mQxAGFiQD4eCcjsZGT44ameGP +uY4zbGneWK2gDqdkVBFpRGZPTBKnjix9xNRbxQA0MMHZmf4yzgeEtE7NCv82TWLx +p2NX5Ntqp66/K7nJ5rInieV+mhxNaMbBGN4zK1FGSxyO9z0M+Yo0FMT7MzUj8czx +Kselu7Cizv5Ta01BG2Yospb6p64KTrk5M0ScdMGTHPjgniQlQ/GbI4Kq3ywgsNw2 +TgOzfALU5nsaqocTvz6hdLubDuHAk5/XpGbKuxs74zD0M1mKB3IDVedzagMxbm+W +G+Oin6+Sx+31QrclTDsTBM8clq8cIqPQqwWyTBIjUtz9GVsnnB47ev1CI9sjgBPw +vFEVVJSmdz7QdFG9URQIOTfLHzSpMJ1ShC5VkLG631UAC9hWLbFJSXKAqWLXwPYY +EQRVzXR7z2FwefR7LFxckvzluFqrTJOVoSfupb7PcSNCupt2LQIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUl0YhVyE1 +2jZVx/PxN3DlCPaTKbYwHwYDVR0jBBgwFoAUl0YhVyE12jZVx/PxN3DlCPaTKbYw +DQYJKoZIhvcNAQELBQADggIBAB3dAmB84DWn5ph76kTOZ0BP8pNuZtQ5iSas000E +PLuHIT839HEl2ku6q5aCgZG27dmxpGWX4m9kWaSW7mDKHyP7Rbr/jyTwyqkxf3kf +gLMtMrpkZ2CvuVnN35pJ06iCsfmYlIrM4LvgBBuZYLFGZdwIorJGnkSI6pN+VxbS +FXJfLkur1J1juONI5f6ELlgKn0Md/rcYkoZDSw6cMoYsYPXpSOqV7XAp8dUv/TW0 +V8/bhUiZucJvbI/NeJWsZCj9VrDDb8O+WVLhX4SPgPL0DTatdrOjteFkdjpY3H1P +XlZs5VVZV6Xf8YpmMIzUUmI4d7S+KNfKNsSbBfD4Fdvb8e80nR14SohWZ25g/4/I +i+GOvUKpMwpZQhISKvqxnUOOBZuZ2mKtVzazHbYNeS2WuOvyDEsMpZTGMKcmGS3t +TAZQMPH9WD25SxdfGbRqhFS0OE85og2WaMMolP3tLR9Ka0OWLpABEPs4poEL0L91 +09S5zvE/bw4cHjdx5RiHdRk/ULlepEU0rbDK5uUTdg8xFKmOLZTW1YVNcxVPS/Ky +Pu1svf0OnWZzsD2097+o4BGkxK51CUpjAEggpsadCwmKtODmzj7HPiY46SvepghJ +AwSQiumPv+i2tCqjI40cHLI5kqiPAlxAOXXUc0ECd97N4EOH1uS6SsNsEn/+KuYj +1oxx +-----END CERTIFICATE----- + +# OpenTrust Root CA G2 +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgISESChaRu/vbm9UpaPI+hIvyYRMA0GCSqGSIb3DQEBDQUA +MEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w +ZW5UcnVzdCBSb290IENBIEcyMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAw +MFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU +T3BlblRydXN0IFJvb3QgQ0EgRzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQDMtlelM5QQgTJT32F+D3Y5z1zCU3UdSXqWON2ic2rxb95eolq5cSG+Ntmh +/LzubKh8NBpxGuga2F8ORAbtp+Dz0mEL4DKiltE48MLaARf85KxP6O6JHnSrT78e +CbY2albz4e6WiWYkBuTNQjpK3eCasMSCRbP+yatcfD7J6xcvDH1urqWPyKwlCm/6 +1UWY0jUJ9gNDlP7ZvyCVeYCYitmJNbtRG6Q3ffyZO6v/v6wNj0OxmXsWEH4db0fE +FY8ElggGQgT4hNYdvJGmQr5J1WqIP7wtUdGejeBSzFfdNTVY27SPJIjki9/ca1TS +gSuyzpJLHB9G+h3Ykst2Z7UJmQnlrBcUVXDGPKBWCgOz3GIZ38i1MH/1PCZ1Eb3X +G7OHngevZXHloM8apwkQHZOJZlvoPGIytbU6bumFAYueQ4xncyhZW+vj3CzMpSZy +YhK05pyDRPZRpOLAeiRXyg6lPzq1O4vldu5w5pLeFlwoW5cZJ5L+epJUzpM5ChaH +vGOz9bGTXOBut9Dq+WIyiET7vycotjCVXRIouZW+j1MY5aIYFuJWpLIsEPUdN6b4 +t/bQWVyJ98LVtZR00dX+G7bw5tYee9I8y6jj9RjzIR9u701oBnstXW5DiabA+aC/ +gh7PU3+06yzbXfZqfUAkBXKJOAGTy3HCOV0GEfZvePg3DTmEJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUajn6QiL3 +5okATV59M4PLuG53hq8wHwYDVR0jBBgwFoAUajn6QiL35okATV59M4PLuG53hq8w +DQYJKoZIhvcNAQENBQADggIBAJjLq0A85TMCl38th6aP1F5Kr7ge57tx+4BkJamz +Gj5oXScmp7oq4fBXgwpkTx4idBvpkF/wrM//T2h6OKQQbA2xx6R3gBi2oihEdqc0 +nXGEL8pZ0keImUEiyTCYYW49qKgFbdEfwFFEVn8nNQLdXpgKQuswv42hm1GqO+qT +RmTFAHneIWv2V6CG1wZy7HBGS4tz3aAhdT7cHcCP009zHIXZ/n9iyJVvttN7jLpT +wm+bREx50B1ws9efAvSyB7DH5fitIw6mVskpEndI2S9G/Tvw/HRwkqWOOAgfZDC2 +t0v7NqwQjqBSM2OdAzVWxWm9xiNaJ5T2pBL4LTM8oValX9YZ6e18CL13zSdkzJTa +TkZQh+D5wVOAHrut+0dSixv9ovneDiK3PTNZbNTe9ZUGMg1RGUFcPk8G97krgCf2 +o6p6fAbhQ8MTOWIaNr3gKC6UAuQpLmBVrkA9sHSSXvAgZJY/X0VdiLWK2gKgW0VU +3jg9CcCoSmVGFvyqv1ROTVu+OEO3KMqLM6oaJbolXCkvW0pujOotnCr2BXbgd5eA +iN1nE28daCSLT7d0geX0YJ96Vdc+N9oWaz53rK4YcJUIeSkDiv7BO7M/Gg+kO14f +WKGVyasvc0rQLW6aWQ9VGHgtPFGml4vmu7JwqkwR3v98KzfUetF3NI/n+UL3PIEM +S1IK +-----END CERTIFICATE----- + +# OpenTrust Root CA G3 +-----BEGIN CERTIFICATE----- +MIICITCCAaagAwIBAgISESDm+Ez8JLC+BUCs2oMbNGA/MAoGCCqGSM49BAMDMEAx +CzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9wZW5U +cnVzdCBSb290IENBIEczMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAwMFow +QDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwUT3Bl +blRydXN0IFJvb3QgQ0EgRzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARK7liuTcpm +3gY6oxH84Bjwbhy6LTAMidnW7ptzg6kjFYwvWYpa3RTqnVkrQ7cG7DK2uu5Bta1d +oYXM6h0UZqNnfkbilPPntlahFVmhTzeXuSIevRHr9LIfXsMUmuXZl5mjYzBhMA4G +A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRHd8MUi2I5 +DMlv4VBN0BBY3JWIbTAfBgNVHSMEGDAWgBRHd8MUi2I5DMlv4VBN0BBY3JWIbTAK +BggqhkjOPQQDAwNpADBmAjEAj6jcnboMBBf6Fek9LykBl7+BFjNAk2z8+e2AcG+q +j9uEwov1NcoG3GRvaBbhj5G5AjEA2Euly8LQCGzpGPta3U1fJAuwACEl74+nBCZx +4nxp5V2a+EEfOzmTk51V6s2N8fvB +-----END CERTIFICATE----- + +# QuoVadis Root CA 1 G3 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# QuoVadis Root CA 2 +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +# QuoVadis Root CA +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz +MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw +IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR +dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp +li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D +rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ +WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug +F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU +xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC +Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv +dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw +ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl +IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh +c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy +ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI +KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T +KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq +y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p +dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD +VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk +fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8 +7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R +cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y +mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW +xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK +SnQ2+Q== +-----END CERTIFICATE----- + +# QuoVadis Root CA 2 G3 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# QuoVadis Root CA 3 +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +# QuoVadis Root CA 3 G3 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# Secure Global CA +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +# SecureSign RootCA11 +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr +MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG +A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0 +MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp +Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD +QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz +i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8 +h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV +MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9 +UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni +8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC +h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD +VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB +AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm +KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ +X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr +QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5 +pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN +QSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +# SecureTrust CA +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +# Security Communication Root CA +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY +MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t +dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 +WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD +VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 +9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ +DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 +Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N +QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ +xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G +A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG +kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr +Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 +Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU +JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot +RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== +-----END CERTIFICATE----- + +# Security Communication RootCA2 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Sonera Class 2 Root CA +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP +MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx +MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV +BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o +Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt +5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s +3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej +vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu +8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw +DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG +MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil +zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/ +3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD +FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6 +Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2 +ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M +-----END CERTIFICATE----- + +# SSL.com EV Root Certification Authority ECC +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +# SSL.com EV Root Certification Authority RSA R2 +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +# SSL.com Root Certification Authority ECC +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +# SSL.com Root Certification Authority RSA +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +# Staat der Nederlanden EV Root CA +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y +MjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg +TmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS +b290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS +M4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC +UiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d +Z//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p +rfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l +pJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb +j5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC +KFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS +/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X +cgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH +1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP +px9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7 +MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u +2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS +v4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC +wPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy +CqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e +vTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6 +Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa +Gl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL +eG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8 +FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc +7uzXLg== +-----END CERTIFICATE----- + +# Staat der Nederlanden Root CA - G2 +-----BEGIN CERTIFICATE----- +MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX +DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291 +qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp +uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU +Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE +pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp +5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M +UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN +GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy +5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv +6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK +eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6 +B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/ +BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov +L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG +SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS +CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen +5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897 +IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK +gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL ++63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL +vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm +bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk +N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC +Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z +ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ== +-----END CERTIFICATE----- + +# Staat der Nederlanden Root CA - G3 +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX +DTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP +cPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW +IkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX +xz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy +KJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR +9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az +5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8 +6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7 +Ngzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP +bMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt +BznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt +XUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd +INyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp +LiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8 +Ipf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp +gZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh +/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw +0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A +fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq +4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR +1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/ +QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM +94B7IWcnMFk= +-----END CERTIFICATE----- + +# Starfield Class 2 CA +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +# Starfield Root Certificate Authority - G2 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Starfield Services Root Certificate Authority - G2 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# SwissSign Gold CA - G2 +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +# SwissSign Silver CA - G2 +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE +BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu +IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow +RzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY +U3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv +Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br +YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF +nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH +6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt +eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/ +c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ +MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH +HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf +jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6 +5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB +rDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +F6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c +wpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB +AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp +WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9 +xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ +2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ +IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8 +aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X +em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR +dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/ +OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+ +hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy +tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- + +# SZAFIR ROOT CA2 +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +# Taiwan GRCA +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/ +MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow +PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR +IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q +gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy +yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts +F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2 +jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx +ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC +VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK +YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH +EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN +Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud +DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE +MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK +UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ +TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf +qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK +ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE +JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7 +hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1 +EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm +nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX +udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz +ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe +LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl +pYYsfPQS +-----END CERTIFICATE----- + +# TeliaSonera Root CA v1 +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +# thawte Primary Root CA +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +# thawte Primary Root CA - G2 +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp +IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi +BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw +MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig +YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v +dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ +BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 +papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K +DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 +KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox +XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +# thawte Primary Root CA - G3 +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB +rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV +BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa +Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl +LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u +MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm +gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 +YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf +b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 +9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S +zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk +OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA +2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW +oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c +KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM +m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu +MdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +# TrustCor ECA-1 +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYD +VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk +MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxFzAVBgNVBAMMDlRydXN0Q29y +IEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3MjgwN1owgZwxCzAJBgNV +BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw +IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy +dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3Ig +RUNBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb +3w9U73NjKYKtR8aja+3+XzP4Q1HpGjORMRegdMTUpwHmspI+ap3tDvl0mEDTPwOA +BoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23xFUfJ3zSCNV2HykVh0A5 +3ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmcp0yJF4Ou +owReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/ +wZ0+fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZF +ZtS6mFjBAgMBAAGjYzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAf +BgNVHSMEGDAWgBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/ +MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAQEABT41XBVwm8nHc2Fv +civUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u/ukZMjgDfxT2 +AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F +hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50 +soIipX1TH0XsJ5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BI +WJZpTdwHjFGTot+fDz2LYLSCjaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1Wi +tJ/X5g== +-----END CERTIFICATE----- + +# TrustCor RootCert CA-1 +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYD +VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk +MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29y +IFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkxMjMxMTcyMzE2WjCB +pDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFuYW1h +IENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUG +A1UECwweVHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZU +cnVzdENvciBSb290Q2VydCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAv463leLCJhJrMxnHQFgKq1mqjQCj/IDHUHuO1CAmujIS2CNUSSUQIpid +RtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4pQa81QBeCQryJ3pS/C3V +seq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0JEsq1pme +9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CV +EY4hgLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorW +hnAbJN7+KIor0Gqw/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/ +DeOxCbeKyKsZn3MzUOcwHwYDVR0jBBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5mDo4Nvu7Zp5I +/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf +ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZ +yonnMlo2HD6CqFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djts +L1Ac59v2Z3kf9YKVmgenFK+P3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdN +zl/HHk484IkzlQsPpTLWPFp5LBk= +-----END CERTIFICATE----- + +# TrustCor RootCert CA-2 +-----BEGIN CERTIFICATE----- +MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNV +BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw +IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy +dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEfMB0GA1UEAwwWVHJ1c3RDb3Ig +Um9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEyMzExNzI2MzlaMIGk +MQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEg +Q2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYD +VQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRy +dXN0Q29yIFJvb3RDZXJ0IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCnIG7CKqJiJJWQdsg4foDSq8GbZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+ +QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9NkRvRUqdw6VC0xK5mC8tkq +1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1oYxOdqHp +2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nK +DOObXUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hape +az6LMvYHL1cEksr1/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF +3wP+TfSvPd9cW436cOGlfifHhi5qjxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88 +oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQPeSghYA2FFn3XVDjxklb9tTNM +g9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+CtgrKAmrhQhJ8Z3 +mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh +8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAd +BgNVHQ4EFgQU2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6U +nrybPZx9mCAZ5YwwYrIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/hOsh80QA9z+LqBrWyOrsGS2h60COX +dKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnpkpfbsEZC89NiqpX+ +MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv2wnL +/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RX +CI/hOWB3S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYa +ZH9bDTMJBzN7Bj8RpFxwPIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW +2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dvDDqPys/cA8GiCcjl/YBeyGBCARsaU1q7 +N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYURpFHmygk71dSTlxCnKr3 +Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANExdqtvArB +As8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp +5KeXRKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu +1uwJ +-----END CERTIFICATE----- + +# Trustis FPS Root CA +-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL +ExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx +MzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc +MBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+ +AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH +iTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj +vSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA +0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB +OrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/ +BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E +FgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01 +GX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW +zaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4 +1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE +f1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F +jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN +ZetX2fNXlrtIzYE= +-----END CERTIFICATE----- + +# T-TeleSec GlobalRoot Class 2 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# T-TeleSec GlobalRoot Class 3 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +# TWCA Global Root CA +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# TWCA Root Certification Authority +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# USERTrust ECC Certification Authority +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# USERTrust RSA Certification Authority +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +# Verisign Class 3 Public Primary Certification Authority - G3 +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +# VeriSign Class 3 Public Primary Certification Authority - G4 +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp +U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg +SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln +biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm +GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve +fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ +aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj +aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW +kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC +4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga +FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +# VeriSign Class 3 Public Primary Certification Authority - G5 +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +# VeriSign Universal Root Certification Authority +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB +vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W +ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0 +IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y +IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh +bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF +9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH +H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H +LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN +/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT +rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw +WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs +exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4 +sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+ +seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz +4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+ +BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR +lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 +7M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +# Visa eCommerce Root +-----BEGIN CERTIFICATE----- +MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr +MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl +cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv +bW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2WhcNMjIwNjI0MDAxNjEyWjBrMQsw +CQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5h +dGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1l +cmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h +2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E +lpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV +ZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq +299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI/k4+oKsGGelT84ATB+0t +vz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL +dXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUF +AAOCAQEAX/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcR +zCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3 +LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd +7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQa7FkKMcPcw +++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt +398znM/jra6O1I7mT1GvFpLgXPYHDw== +-----END CERTIFICATE----- + +# XRamp Global CA Root +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +# CAcert Class 3 Root +-----BEGIN CERTIFICATE----- +MIIHWTCCBUGgAwIBAgIDCkGKMA0GCSqGSIb3DQEBCwUAMHkxEDAOBgNVBAoTB1Jv +b3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEiMCAGA1UEAxMZ +Q0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYSc3VwcG9y +dEBjYWNlcnQub3JnMB4XDTExMDUyMzE3NDgwMloXDTIxMDUyMDE3NDgwMlowVDEU +MBIGA1UEChMLQ0FjZXJ0IEluYy4xHjAcBgNVBAsTFWh0dHA6Ly93d3cuQ0FjZXJ0 +Lm9yZzEcMBoGA1UEAxMTQ0FjZXJ0IENsYXNzIDMgUm9vdDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAKtJNRFIfNImflOUz0Op3SjXQiqL84d4GVh8D57a +iX3h++tykA10oZZkq5+gJJlz2uJVdscXe/UErEa4w75/ZI0QbCTzYZzA8pD6Ueb1 +aQFjww9W4kpCz+JEjCUoqMV5CX1GuYrz6fM0KQhF5Byfy5QEHIGoFLOYZcRD7E6C +jQnRvapbjZLQ7N6QxX8KwuPr5jFaXnQ+lzNZ6MMDPWAzv/fRb0fEze5ig1JuLgia +pNkVGJGmhZJHsK5I6223IeyFGmhyNav/8BBdwPSUp2rVO5J+TJAFfpPBLIukjmJ0 +FXFuC3ED6q8VOJrU0gVyb4z5K+taciX5OUbjchs+BMNkJyIQKopPWKcDrb60LhPt +XapI19V91Cp7XPpGBFDkzA5CW4zt2/LP/JaT4NsRNlRiNDiPDGCbO5dWOK3z0luL +oFvqTpa4fNfVoIZwQNORKbeiPK31jLvPGpKK5DR7wNhsX+kKwsOnIJpa3yxdUly6 +R9Wb7yQocDggL9V/KcCyQQNokszgnMyXS0XvOhAKq3A6mJVwrTWx6oUrpByAITGp +rmB6gCZIALgBwJNjVSKRPFbnr9s6JfOPMVTqJouBWfmh0VMRxXudA/Z0EeBtsSw/ +LIaRmXGapneLNGDRFLQsrJ2vjBDTn8Rq+G8T/HNZ92ZCdB6K4/jc0m+YnMtHmJVA +BfvpAgMBAAGjggINMIICCTAdBgNVHQ4EFgQUdahxYEyIE/B42Yl3tW3Fid+8sXow +gaMGA1UdIwSBmzCBmIAUFrUyG9TH8+DmjvO90rA67rI5GNGhfaR7MHkxEDAOBgNV +BAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEiMCAG +A1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYS +c3VwcG9ydEBjYWNlcnQub3JnggEAMA8GA1UdEwEB/wQFMAMBAf8wXQYIKwYBBQUH +AQEEUTBPMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5DQWNlcnQub3JnLzAoBggr +BgEFBQcwAoYcaHR0cDovL3d3dy5DQWNlcnQub3JnL2NhLmNydDBKBgNVHSAEQzBB +MD8GCCsGAQQBgZBKMDMwMQYIKwYBBQUHAgEWJWh0dHA6Ly93d3cuQ0FjZXJ0Lm9y +Zy9pbmRleC5waHA/aWQ9MTAwNAYJYIZIAYb4QgEIBCcWJWh0dHA6Ly93d3cuQ0Fj +ZXJ0Lm9yZy9pbmRleC5waHA/aWQ9MTAwUAYJYIZIAYb4QgENBEMWQVRvIGdldCB5 +b3VyIG93biBjZXJ0aWZpY2F0ZSBmb3IgRlJFRSwgZ28gdG8gaHR0cDovL3d3dy5D +QWNlcnQub3JnMA0GCSqGSIb3DQEBCwUAA4ICAQApKIWuRKm5r6R5E/CooyuXYPNc +7uMvwfbiZqARrjY3OnYVBFPqQvX56sAV2KaC2eRhrnILKVyQQ+hBsuF32wITRHhH +Va9Y/MyY9kW50SD42CEH/m2qc9SzxgfpCYXMO/K2viwcJdVxjDm1Luq+GIG6sJO4 +D+Pm1yaMMVpyA4RS5qb1MyJFCsgLDYq4Nm+QCaGrvdfVTi5xotSu+qdUK+s1jVq3 +VIgv7nSf7UgWyg1I0JTTrKSi9iTfkuO960NAkW4cGI5WtIIS86mTn9S8nK2cde5a +lxuV53QtHA+wLJef+6kzOXrnAzqSjiL2jA3k2X4Ndhj3AfnvlpaiVXPAPHG0HRpW +Q7fDCo1y/OIQCQtBzoyUoPkD/XFzS4pXM+WOdH4VAQDmzEoc53+VGS3FpQyLu7Xt +hbNc09+4ufLKxw0BFKxwWMWMjTPUnWajGlCVI/xI4AZDEtnNp4Y5LzZyo4AQ5OHz +0ctbGsDkgJp8E3MGT9ujayQKurMcvEp4u+XjdTilSKeiHq921F73OIZWWonO1sOn +ebJSoMbxhbQljPI/lrMQ2Y1sVzufb4Y6GIIiNsiwkTjbKqGTqoQ/9SdlrnPVyNXT +d+pLncdBu8fA46A/5H2kjXPmEkvfoXNzczqA6NXLji/L6hOn1kGLrPo8idck9U60 +4GGSt/M3mMS+lqO3ig== +-----END CERTIFICATE----- + +# CA Cert Signing Authority +-----BEGIN CERTIFICATE----- +MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290 +IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB +IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA +Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO +BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi +MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ +ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ +8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6 +zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y +fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7 +w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc +G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k +epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q +laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ +QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU +fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826 +YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w +ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY +gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe +MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0 +IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy +dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw +czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0 +dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl +aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC +AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg +b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB +ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc +nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg +18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c +gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl +Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY +sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T +SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF +CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum +GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk +zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW +omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD +-----END CERTIFICATE----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_Certification_Authority.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_Certification_Authority.pem new file mode 100644 index 000000000..6146dcb57 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_Certification_Authority.pem @@ -0,0 +1,25 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_ECC_Certification_Authority.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_ECC_Certification_Authority.pem new file mode 100644 index 000000000..546c95e30 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_ECC_Certification_Authority.pem @@ -0,0 +1,16 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_RSA_Certification_Authority.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_RSA_Certification_Authority.pem new file mode 100644 index 000000000..6508d1e8c --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/COMODO_RSA_Certification_Authority.pem @@ -0,0 +1,34 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/DST_Root_CA_X3.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/DST_Root_CA_X3.pem new file mode 100644 index 000000000..b2e43c938 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/DST_Root_CA_X3.pem @@ -0,0 +1,20 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/ISRG_Root_X1.pem b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/ISRG_Root_X1.pem new file mode 100644 index 000000000..b85c8037f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/files/roots/ISRG_Root_X1.pem @@ -0,0 +1,31 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/meta/main.yml new file mode 100644 index 000000000..847630802 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - prepare_jinja2_compat + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/create-single-certificate.yml b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/create-single-certificate.yml new file mode 100644 index 000000000..fbeac4e38 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/create-single-certificate.yml @@ -0,0 +1,25 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Generate CSR for {{ certificate.name }} + openssl_csr: + path: '{{ remote_tmp_dir }}/{{ certificate.name }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/{{ certificate.name }}.key' + subject: '{{ certificate.subject }}' + useCommonNameForSAN: false + +- name: Generate certificate for {{ certificate.name }} + x509_certificate: + path: '{{ remote_tmp_dir }}/{{ certificate.name }}.pem' + csr_path: '{{ remote_tmp_dir }}/{{ certificate.name }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/{{ certificate.name }}.key' + provider: '{{ "selfsigned" if certificate.parent is not defined else "ownca" }}' + ownca_path: '{{ (remote_tmp_dir ~ "/" ~ certificate.parent ~ ".pem") if certificate.parent is defined else omit }}' + ownca_privatekey_path: '{{ (remote_tmp_dir ~ "/" ~ certificate.parent ~ ".key") if certificate.parent is defined else omit }}' diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/create.yml b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/create.yml new file mode 100644 index 000000000..d05859f83 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/create.yml @@ -0,0 +1,54 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- block: + - name: Create private keys + openssl_privatekey: + path: '{{ remote_tmp_dir }}/{{ item.name }}.key' + size: '{{ default_rsa_key_size_certifiates }}' + loop: '{{ certificates }}' + + - name: Generate certificates + include_tasks: create-single-certificate.yml + loop: '{{ certificates }}' + loop_control: + loop_var: certificate + + - name: Read certificates + slurp: + src: '{{ remote_tmp_dir }}/{{ item.name }}.pem' + loop: '{{ certificates }}' + register: certificates_read + + - name: Store read certificates + set_fact: + read_certificates: >- + {{ certificates_read.results | map(attribute='content') | map('b64decode') + | zip(certificates | map(attribute='name')) + | list + | items2dict(key_name=1, value_name=0) }} + + vars: + certificates: + - name: a-root + subject: + commonName: root common name + - name: b-intermediate + subject: + commonName: intermediate common name + parent: a-root + - name: c-intermediate + subject: + commonName: intermediate common name + parent: a-root + - name: d-leaf + subject: + commonName: leaf certificate + parent: b-intermediate diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/created.yml b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/created.yml new file mode 100644 index 000000000..bbd86c6a7 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/created.yml @@ -0,0 +1,49 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Case A => works + certificate_complete_chain: + input_chain: "{{ read_certificates['d-leaf'] }}" + intermediate_certificates: + - '{{ remote_tmp_dir }}/b-intermediate.pem' + root_certificates: + - '{{ remote_tmp_dir }}/a-root.pem' + +- name: Case B => doesn't work, but this is expected + failed_when: false + register: caseb + certificate_complete_chain: + input_chain: "{{ read_certificates['d-leaf'] }}" + intermediate_certificates: + - '{{ remote_tmp_dir }}/c-intermediate.pem' + root_certificates: + - '{{ remote_tmp_dir }}/a-root.pem' + +- name: Assert that case B failed + assert: + that: "'Cannot complete chain' in caseb.msg" + +- name: Case C => works + certificate_complete_chain: + input_chain: "{{ read_certificates['d-leaf'] }}" + intermediate_certificates: + - '{{ remote_tmp_dir }}/c-intermediate.pem' + - '{{ remote_tmp_dir }}/b-intermediate.pem' + root_certificates: + - '{{ remote_tmp_dir }}/a-root.pem' + +- name: Case D => works as well after PR 403 + certificate_complete_chain: + input_chain: "{{ read_certificates['d-leaf'] }}" + intermediate_certificates: + - '{{ remote_tmp_dir }}/b-intermediate.pem' + - '{{ remote_tmp_dir }}/c-intermediate.pem' + root_certificates: + - '{{ remote_tmp_dir }}/a-root.pem' diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/existing.yml b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/existing.yml new file mode 100644 index 000000000..a5c47ece9 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/existing.yml @@ -0,0 +1,149 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- block: + - name: Find root for cert 1 using directory + certificate_complete_chain: + input_chain: '{{ fullchain | trim }}' + root_certificates: + - '{{ remote_tmp_dir }}/files/roots/' + register: cert1_root + - name: Verify root for cert 1 + assert: + that: + - cert1_root.complete_chain | join('') == (fullchain ~ root) + - cert1_root.root == root + vars: + fullchain: "{{ lookup('file', 'cert1-fullchain.pem', rstrip=False) }}" + root: "{{ lookup('file', 'cert1-root.pem', rstrip=False) }}" + +- block: + - name: Find rootchain for cert 1 using intermediate and root PEM + certificate_complete_chain: + input_chain: '{{ cert }}' + 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('') == (cert ~ chain ~ root) + - cert1_rootchain.chain[:-1] | join('') == chain + - cert1_rootchain.root == root + vars: + cert: "{{ lookup('file', 'cert1.pem', rstrip=False) }}" + chain: "{{ lookup('file', 'cert1-chain.pem', rstrip=False) }}" + root: "{{ lookup('file', 'cert1-root.pem', rstrip=False) }}" + +- block: + - name: Find root for cert 2 using directory + certificate_complete_chain: + input_chain: "{{ fullchain | trim }}" + root_certificates: + - '{{ remote_tmp_dir }}/files/roots/' + register: cert2_root + - name: Verify root for cert 2 + assert: + that: + - cert2_root.complete_chain | join('') == (fullchain ~ root) + - cert2_root.root == root + vars: + fullchain: "{{ lookup('file', 'cert2-fullchain.pem', rstrip=False) }}" + root: "{{ lookup('file', 'cert2-root.pem', rstrip=False) }}" + +- block: + - name: Find rootchain for cert 2 using intermediate and root PEM + certificate_complete_chain: + input_chain: '{{ cert }}' + 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('') == (cert ~ chain ~ root) + - cert2_rootchain.chain[:-1] | join('') == chain + - cert2_rootchain.root == root + vars: + cert: "{{ lookup('file', 'cert2.pem', rstrip=False) }}" + chain: "{{ lookup('file', 'cert2-chain.pem', rstrip=False) }}" + root: "{{ lookup('file', 'cert2-root.pem', rstrip=False) }}" + +- block: + - name: Find alternate rootchain for cert 2 using intermediate and root PEM + certificate_complete_chain: + input_chain: '{{ cert }}' + 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('') == (cert ~ chain ~ root) + - cert2_rootchain_alt.chain[:-1] | join('') == chain + - cert2_rootchain_alt.root == root + vars: + cert: "{{ lookup('file', 'cert2.pem', rstrip=False) }}" + chain: "{{ lookup('file', 'cert2-altchain.pem', rstrip=False) }}" + root: "{{ lookup('file', 'cert2-altroot.pem', rstrip=False) }}" + +- block: + - name: Find alternate rootchain for cert 2 when complete chain is already presented to the module + certificate_complete_chain: + input_chain: '{{ cert ~ chain ~ root }}' + root_certificates: + - '{{ remote_tmp_dir }}/files/roots.pem' + register: cert2_complete_chain + - name: Verify rootchain for cert 2 + assert: + that: + - cert2_complete_chain.complete_chain | join('') == (cert ~ chain ~ root) + - cert2_complete_chain.chain == [] + - cert2_complete_chain.root == root + vars: + cert: "{{ lookup('file', 'cert2.pem', rstrip=False) }}" + chain: "{{ lookup('file', 'cert2-altchain.pem', rstrip=False) }}" + root: "{{ lookup('file', 'cert2-altroot.pem', rstrip=False) }}" + +- name: Check failure when no intermediate certificate can be found + certificate_complete_chain: + input_chain: '{{ lookup("file", "cert2.pem", rstrip=True) }}' + intermediate_certificates: + - '{{ remote_tmp_dir }}/files/cert1-chain.pem' + root_certificates: + - '{{ remote_tmp_dir }}/files/roots.pem' + register: cert2_no_intermediate + ignore_errors: true +- name: Verify failure + assert: + that: + - cert2_no_intermediate is failed + - "cert2_no_intermediate.msg.startswith('Cannot complete chain. Stuck at certificate ')" + +- name: Check failure when infinite loop is found + certificate_complete_chain: + input_chain: '{{ lookup("file", "cert1-fullchain.pem", rstrip=True) }}' + intermediate_certificates: + - '{{ remote_tmp_dir }}/files/roots.pem' + root_certificates: + - '{{ remote_tmp_dir }}/files/cert2-chain.pem' + register: cert2_infinite_loop + ignore_errors: true +- name: Verify failure + assert: + that: + - cert2_infinite_loop is failed + - "cert2_infinite_loop.msg == 'Found cycle while building certificate chain'" diff --git a/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/main.yml new file mode 100644 index 000000000..fbb8553d5 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/certificate_complete_chain/tasks/main.yml @@ -0,0 +1,32 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- block: + + - name: Make sure testhost directory exists + file: + path: '{{ remote_tmp_dir }}/files/' + state: directory + when: ansible_version.string is version('2.10', '<') + - name: Copy test files to testhost + copy: + src: '{{ role_path }}/files/' + dest: '{{ remote_tmp_dir }}/files/' + + - name: Run tests with copied certificates + import_tasks: existing.yml + + - name: Create more certificates + import_tasks: create.yml + + - name: Run tests with created certificates + import_tasks: created.yml + + when: cryptography_version.stdout is version('1.5', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/crypto_info/aliases b/ansible_collections/community/crypto/tests/integration/targets/crypto_info/aliases new file mode 100644 index 000000000..00bbb3ddd --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/crypto_info/aliases @@ -0,0 +1,8 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +context/controller +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/crypto_info/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/crypto_info/meta/main.yml new file mode 100644 index 000000000..597f9fd97 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/crypto_info/meta/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl diff --git a/ansible_collections/community/crypto/tests/integration/targets/crypto_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/crypto_info/tasks/main.yml new file mode 100644 index 000000000..defb74119 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/crypto_info/tasks/main.yml @@ -0,0 +1,79 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Retrieve information + crypto_info: + register: result + +- name: Display information + debug: + var: result + +- name: Register cryptography version + command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'" + register: local_cryptography_version + +- name: Determine complex version-based capabilities + set_fact: + supports_ed25519: >- + {{ + local_cryptography_version.stdout is version("2.6", ">=") + and not ( + ansible_os_family == "FreeBSD" and + ansible_facts.distribution_version is version("12.1", ">=") and + ansible_facts.distribution_version is version("12.2", "<") + ) + }} + supports_ed448: >- + {{ + local_cryptography_version.stdout is version("2.6", ">=") + and not ( + ansible_os_family == "FreeBSD" and + ansible_facts.distribution_version is version("12.1", ">=") and + ansible_facts.distribution_version is version("12.2", "<") + ) + }} + +- name: Verify cryptography information + assert: + that: + - result.python_cryptography_installed + - "'python_cryptography_import_error' not in result" + - result.python_cryptography_capabilities.version == local_cryptography_version.stdout + - "'secp256r1' in result.python_cryptography_capabilities.curves" + - result.python_cryptography_capabilities.has_ec == (local_cryptography_version.stdout is version('0.5', '>=')) + - result.python_cryptography_capabilities.has_ec_sign == (local_cryptography_version.stdout is version('1.5', '>=')) + - result.python_cryptography_capabilities.has_ed25519 == supports_ed25519 + - result.python_cryptography_capabilities.has_ed25519_sign == supports_ed25519 + - result.python_cryptography_capabilities.has_ed448 == supports_ed448 + - result.python_cryptography_capabilities.has_ed448_sign == supports_ed448 + - result.python_cryptography_capabilities.has_dsa == (local_cryptography_version.stdout is version('0.5', '>=')) + - result.python_cryptography_capabilities.has_dsa_sign == (local_cryptography_version.stdout is version('1.5', '>=')) + - result.python_cryptography_capabilities.has_rsa == (local_cryptography_version.stdout is version('0.5', '>=')) + - result.python_cryptography_capabilities.has_rsa_sign == (local_cryptography_version.stdout is version('1.4', '>=')) + - result.python_cryptography_capabilities.has_x25519 == (local_cryptography_version.stdout is version('2.0', '>=')) + - result.python_cryptography_capabilities.has_x25519_serialization == (local_cryptography_version.stdout is version('2.5', '>=')) + - result.python_cryptography_capabilities.has_x448 == (local_cryptography_version.stdout is version('2.5', '>=')) + +- name: Find OpenSSL binary + command: which openssl + register: local_openssl_path + +- name: Find OpenSSL version + command: openssl version + register: local_openssl_version_full + +- name: Verify OpenSSL information + assert: + that: + - result.openssl_present + - result.openssl.path == local_openssl_path.stdout + - (result.openssl.version_output | trim) == local_openssl_version_full.stdout + - result.openssl.version == local_openssl_version_full.stdout.split(' ')[1] diff --git a/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/aliases b/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/aliases new file mode 100644 index 000000000..12273cafd --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/aliases @@ -0,0 +1,19 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# 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 diff --git a/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/defaults/main.yml b/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/defaults/main.yml new file mode 100644 index 000000000..d42aab015 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/defaults/main.yml @@ -0,0 +1,6 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# defaults file for test_ecs_certificate diff --git a/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/meta/main.yml new file mode 100644 index 000000000..b7fbb90f9 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - prepare_tests + - setup_openssl diff --git a/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/tasks/main.yml new file mode 100644 index 000000000..ad74aa34f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/tasks/main.yml @@ -0,0 +1,224 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +## 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_binary }} 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 diff --git a/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/vars/main.yml b/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/vars/main.yml new file mode 100644 index 000000000..ae9eeb5d1 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/ecs_certificate/vars/main.yml @@ -0,0 +1,56 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# 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' diff --git a/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/aliases b/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/aliases new file mode 100644 index 000000000..12273cafd --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/aliases @@ -0,0 +1,19 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# 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 diff --git a/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/defaults/main.yml b/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/defaults/main.yml new file mode 100644 index 000000000..136561106 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/defaults/main.yml @@ -0,0 +1,6 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# defaults file for test_ecs_domain diff --git a/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/meta/main.yml new file mode 100644 index 000000000..368ea207d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/meta/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - prepare_tests diff --git a/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/tasks/main.yml new file mode 100644 index 000000000..f11910981 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/tasks/main.yml @@ -0,0 +1,279 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +## 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 diff --git a/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/vars/main.yml b/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/vars/main.yml new file mode 100644 index 000000000..71bf27031 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/ecs_domain/vars/main.yml @@ -0,0 +1,19 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# 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' diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_csr_info/aliases b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_csr_info/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_csr_info/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_csr_info/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_csr_info/meta/main.yml new file mode 100644 index 000000000..7c2b42405 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_csr_info/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir + - prepare_jinja2_compat diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_csr_info/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_csr_info/tasks/impl.yml new file mode 100644 index 000000000..0af558932 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_csr_info/tasks/impl.yml @@ -0,0 +1,144 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "Get CSR info" + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/csr_1.csr') | community.crypto.openssl_csr_info }} + result_idna: >- + {{ lookup('file', remote_tmp_dir ~ '/csr_1.csr') | community.crypto.openssl_csr_info(name_encoding='idna') }} + result_unicode: >- + {{ lookup('file', remote_tmp_dir ~ '/csr_1.csr') | community.crypto.openssl_csr_info(name_encoding='unicode') }} + +- name: "Check whether subject and extensions behaves as expected" + assert: + that: + - result.subject.organizationalUnitName == 'ACME Department' + - "['organizationalUnitName', 'Crypto Department'] in result.subject_ordered" + - "['organizationalUnitName', 'ACME Department'] in result.subject_ordered" + - result.public_key_type == 'RSA' + - result.public_key_data.size == default_rsa_key_size + # TLS Feature + - result.extensions_by_oid['1.3.6.1.5.5.7.1.24'].critical == false + - result.extensions_by_oid['1.3.6.1.5.5.7.1.24'].value == 'MAMCAQU=' + # Key Usage + - result.extensions_by_oid['2.5.29.15'].critical == true + - result.extensions_by_oid['2.5.29.15'].value in ['AwMA/4A=', 'AwMH/4A='] + # Subject Alternative Names + - result.subject_alt_name[1] == ("DNS:âņsïbłè.com" if cryptography_version.stdout is version('2.1', '<') else "DNS:xn--sb-oia0a7a53bya.com") + - result_unicode.subject_alt_name[1] == "DNS:âņsïbłè.com" + - result_idna.subject_alt_name[1] == "DNS:xn--sb-oia0a7a53bya.com" + - result.extensions_by_oid['2.5.29.17'].critical == false + - result.extensions_by_oid['2.5.29.17'].value == 'MHmCD3d3dy5hbnNpYmxlLmNvbYIXeG4tLXNiLW9pYTBhN2E1M2J5YS5jb22HBAECAwSHEAAAAAAAAAAAAAAAAAAAAAGBEHRlc3RAZXhhbXBsZS5vcmeGI2h0dHBzOi8vZXhhbXBsZS5vcmcvdGVzdC9pbmRleC5odG1s' + # Basic Constraints + - result.extensions_by_oid['2.5.29.19'].critical == true + - result.extensions_by_oid['2.5.29.19'].value == 'MAYBAf8CARc=' + # Extended Key Usage + - result.extensions_by_oid['2.5.29.37'].critical == false + - result.extensions_by_oid['2.5.29.37'].value == 'MHQGCCsGAQUFBwMBBggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUFBwMJBgRVHSUABggrBgEFBQcBAwYIKwYBBQUHAwoGCCsGAQUFBwMHBggrBgEFBQcBAg==' + +- name: "Check SubjectKeyIdentifier and AuthorityKeyIdentifier" + assert: + that: + - result.subject_key_identifier == "00:11:22:33" + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + # Subject Key Identifier + - result.extensions_by_oid['2.5.29.14'].critical == false + # Authority Key Identifier + - result.extensions_by_oid['2.5.29.35'].critical == false + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: cryptography_version.stdout is version('1.3', '>=') + +- name: "Get CSR info" + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/csr_2.csr') | community.crypto.openssl_csr_info }} + +- name: "Get CSR info" + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/csr_3.csr') | community.crypto.openssl_csr_info }} + +- name: "Check AuthorityKeyIdentifier" + assert: + that: + - result.authority_key_identifier is none + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: cryptography_version.stdout is version('1.3', '>=') + +- name: "Get CSR info" + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/csr_4.csr') | community.crypto.openssl_csr_info }} + +- name: "Check AuthorityKeyIdentifier" + assert: + that: + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer is none + - result.authority_cert_serial_number is none + when: cryptography_version.stdout is version('1.3', '>=') + +- name: Get invalid certificate info + set_fact: + result: >- + {{ [] | community.crypto.openssl_csr_info }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^The community.crypto.openssl_csr_info input must be a text type, not <(?:class|type) 'list'>$") + +- name: Get invalid certificate info + set_fact: + result: >- + {{ 'foo' | community.crypto.openssl_csr_info }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^Unable to load (?:request|PEM file)(?:\.|$)") + +- name: Get invalid certificate info + set_fact: + result: >- + {{ 'foo' | community.crypto.openssl_csr_info(name_encoding=[]) }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^The name_encoding option must be of a text type, not <(?:class|type) 'list'>$") + +- name: Get invalid name_encoding parameter + set_fact: + result: >- + {{ 'bar' | community.crypto.openssl_csr_info(name_encoding='foo') }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^The name_encoding option must be one of the values \"ignore\", \"idna\", or \"unicode\", not \"foo\"$") diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_csr_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_csr_info/tasks/main.yml new file mode 100644 index 000000000..09446941d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_csr_info/tasks/main.yml @@ -0,0 +1,133 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Make sure the Python idna library is installed + pip: + name: idna + state: present + +- name: Generate privatekey + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey.pem' + size: '{{ default_rsa_key_size }}' + +- name: Generate privatekey with password + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + size: '{{ default_rsa_key_size }}' + +- name: Generate CSR 1 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_1.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.example.com + C: de + L: Somewhere + ST: Zurich + streetAddress: Welcome Street + O: Ansible + organizationalUnitName: + - Crypto Department + - ACME Department + serialNumber: "1234" + SN: Last Name + GN: First Name + title: Chief + pseudonym: test + UID: asdf + emailAddress: test@example.com + postalAddress: 1234 Somewhere + postalCode: "1234" + useCommonNameForSAN: false + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: true + extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + subject_alt_name: + - "DNS:www.ansible.com" + - "DNS:âņsïbłè.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: true + ocsp_must_staple: true + subject_key_identifier: '{{ "00:11:22:33" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + +- name: Generate CSR 2 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_2.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + privatekey_passphrase: hunter2 + useCommonNameForSAN: false + basic_constraints: + - "CA:TRUE" + +- name: Generate CSR 3 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_3.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + useCommonNameForSAN: false + subject_alt_name: + - "DNS:*.ansible.com" + - "DNS:*.example.org" + - "IP:DEAD:BEEF::1" + basic_constraints: + - "CA:FALSE" + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + +- name: Generate CSR 4 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_4.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + useCommonNameForSAN: false + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + +- name: Running tests + include_tasks: impl.yml + when: cryptography_version.stdout is version('1.3', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_privatekey_info/aliases b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_privatekey_info/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_privatekey_info/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_privatekey_info/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_privatekey_info/meta/main.yml new file mode 100644 index 000000000..7c2b42405 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_privatekey_info/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir + - prepare_jinja2_compat diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_privatekey_info/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_privatekey_info/tasks/impl.yml new file mode 100644 index 000000000..d4f5df0af --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_privatekey_info/tasks/impl.yml @@ -0,0 +1,113 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Get key 1 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/privatekey_1.pem') | community.crypto.openssl_privatekey_info }} + +- name: Check that RSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + - "'private_data' not in result" + +- name: Get key 2 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/privatekey_2.pem') | community.crypto.openssl_privatekey_info(return_private_key_data=true) }} + +- name: Check that RSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "result.public_data.size == default_rsa_key_size" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + - "'private_data' in result" + - "result.public_data.modulus == result.private_data.p * result.private_data.q" + - "result.private_data.exponent > 5" + +- name: Get key 3 info (without passphrase) + set_fact: + result_: >- + {{ lookup('file', remote_tmp_dir ~ '/privatekey_3.pem') | community.crypto.openssl_privatekey_info(return_private_key_data=true) }} + ignore_errors: true + register: result + +- name: Check that loading passphrase protected key without passphrase failed + assert: + that: + - result is failed + - result.msg == 'Wrong or empty passphrase provided for private key' + +- name: Get key 3 info (with passphrase) + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/privatekey_3.pem') | community.crypto.openssl_privatekey_info(passphrase='hunter2', return_private_key_data=true) }} + +- name: Check that RSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + - "'private_data' in result" + - "result.public_data.modulus == result.private_data.p * result.private_data.q" + - "result.private_data.exponent > 5" + +- name: Get key 4 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/privatekey_4.pem') | community.crypto.openssl_privatekey_info(return_private_key_data=true) }} + +- name: Check that ECC key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'ECC'" + - "'public_data' in result" + - "result.public_data.curve is string" + - "result.public_data.x != 0" + - "result.public_data.y != 0" + - "result.public_data.exponent_size == (521 if (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') else 256)" + - "'private_data' in result" + - "result.private_data.multiplier > 1024" + +- name: Get key 5 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/privatekey_5.pem') | community.crypto.openssl_privatekey_info(return_private_key_data=true) }} + +- name: Check that DSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'DSA'" + - "'public_data' in result" + - "result.public_data.p > 2" + - "result.public_data.q > 2" + - "result.public_data.g >= 2" + - "result.public_data.y > 2" + - "'private_data' in result" + - "result.private_data.x > 2" diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_privatekey_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_privatekey_info/tasks/main.yml new file mode 100644 index 000000000..fcbd35971 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_privatekey_info/tasks/main.yml @@ -0,0 +1,43 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Generate privatekey 1 + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_1.pem' + +- name: Generate privatekey 2 (less bits) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_2.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + +- name: Generate privatekey 3 (with password) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_3.pem' + passphrase: hunter2 + cipher: auto + size: '{{ default_rsa_key_size }}' + +- name: Generate privatekey 4 (ECC) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_4.pem' + type: ECC + curve: "{{ (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') | ternary('secp521r1', 'secp256k1') }}" + # ^ cryptography on CentOS6 doesn't support secp256k1, so we use secp521r1 instead + +- name: Generate privatekey 5 (DSA) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_5.pem' + type: DSA + size: 1024 + +- name: Running tests + include_tasks: impl.yml + when: cryptography_version.stdout is version('1.2.3', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_publickey_info/aliases b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_publickey_info/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_publickey_info/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_publickey_info/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_publickey_info/meta/main.yml new file mode 100644 index 000000000..7c2b42405 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_publickey_info/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir + - prepare_jinja2_compat diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_publickey_info/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_publickey_info/tasks/impl.yml new file mode 100644 index 000000000..156f2a748 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_publickey_info/tasks/impl.yml @@ -0,0 +1,95 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Get key 1 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/publickey_1.pem') | community.crypto.openssl_publickey_info }} + +- name: Check that RSA key info is ok + assert: + that: + - "'fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + +- name: Get key 2 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/publickey_2.pem') | community.crypto.openssl_publickey_info }} + +- name: Check that RSA key info is ok + assert: + that: + - "'fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "result.public_data.size == default_rsa_key_size" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + +- name: Get key 3 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/publickey_3.pem') | community.crypto.openssl_publickey_info }} + +- name: Check that ECC key info is ok + assert: + that: + - "'fingerprints' in result" + - "'type' in result" + - "result.type == 'ECC'" + - "'public_data' in result" + - "result.public_data.curve is string" + - "result.public_data.x != 0" + - "result.public_data.y != 0" + - "result.public_data.exponent_size == (521 if (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') else 256)" + +- name: Get key 4 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/publickey_4.pem') | community.crypto.openssl_publickey_info }} + +- name: Check that DSA key info is ok + assert: + that: + - "'fingerprints' in result" + - "'type' in result" + - "result.type == 'DSA'" + - "'public_data' in result" + - "result.public_data.p > 2" + - "result.public_data.q > 2" + - "result.public_data.g >= 2" + - "result.public_data.y > 2" + +- name: Get invalid key info + set_fact: + result: >- + {{ [] | community.crypto.openssl_publickey_info }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^The community.crypto.openssl_publickey_info input must be a text type, not <(?:class|type) 'list'>$") + +- name: Get invalid key info + set_fact: + result: >- + {{ 'foo' | community.crypto.openssl_publickey_info }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - 'output.msg is search("^Error while deserializing key: ")' diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_publickey_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_publickey_info/tasks/main.yml new file mode 100644 index 000000000..7375f45a6 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_openssl_publickey_info/tasks/main.yml @@ -0,0 +1,47 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Generate privatekey 1 + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_1.pem' + +- name: Generate privatekey 2 (less bits) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_2.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + +- name: Generate privatekey 3 (ECC) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_3.pem' + type: ECC + curve: "{{ (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') | ternary('secp521r1', 'secp256k1') }}" + # ^ cryptography on CentOS6 doesn't support secp256k1, so we use secp521r1 instead + select_crypto_backend: cryptography + +- name: Generate privatekey 4 (DSA) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_4.pem' + type: DSA + size: 1024 + +- name: Generate public keys + openssl_publickey: + privatekey_path: '{{ remote_tmp_dir }}/privatekey_{{ item }}.pem' + path: '{{ remote_tmp_dir }}/publickey_{{ item }}.pem' + loop: + - 1 + - 2 + - 3 + - 4 + +- name: Running tests + include_tasks: impl.yml + when: cryptography_version.stdout is version('1.2.3', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_split_pem/aliases b/ansible_collections/community/crypto/tests/integration/targets/filter_split_pem/aliases new file mode 100644 index 000000000..857789143 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_split_pem/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_split_pem/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_split_pem/tasks/main.yml new file mode 100644 index 000000000..0b1dfdf1d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_split_pem/tasks/main.yml @@ -0,0 +1,64 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Run tests that raise no errors + assert: + that: + - >- + '' | community.crypto.split_pem == [] + - >- + (pem_1 + pem_2 + pem_3) | community.crypto.split_pem == [pem_1, pem_2, pem_3] + - >- + (pem_3 + pem_2 + pem_1) | community.crypto.split_pem == [pem_3, pem_2, pem_1] + - >- + (crap_1 + pem_3 + crap_2 + pem_2 + crap_3 + pem_1 + crap_2) | community.crypto.split_pem == [pem_3, pem_2, pem_1] + - >- + (crap_1 + pem_1 + crap_2 + pem_1 + crap_3 + crap_4 + crap_4) | community.crypto.split_pem == [pem_1, pem_1] + vars: + pem_1: | + -----BEGIN CERTIFICATE----- + AAb= + -----END CERTIFICATE----- + pem_2: | + -----BEGIN PRIVATE KEY----- + Foo + Bar + Baz + Bam + -----END PRIVATE KEY----- + pem_3: | + -----BEGIN + foo + -----END + crap_1: | + # Comment + crap_2: | + Random text + In multiple + Lines + crap_3: | + ----BEGIN CERTIFICATE---- + Certificate with too few dashes + ----END CERTIFICATE---- + crap_4: | + -----BEGIN CERTIFICATE----- + AAb= + +- name: Invalid input + debug: + msg: "{{ [] | community.crypto.split_pem }}" + ignore_errors: true + register: output + +- name: Validate error + assert: + that: + - output is failed + - output.msg is search("^The community.crypto.split_pem input must be a text type, not <(?:class|type) 'list'>$") diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_x509_certificate_info/aliases b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_certificate_info/aliases new file mode 100644 index 000000000..ca07dd03c --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_certificate_info/aliases @@ -0,0 +1,8 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +needs/target/x509_certificate_info +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_x509_certificate_info/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_certificate_info/meta/main.yml new file mode 100644 index 000000000..7c2b42405 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_certificate_info/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir + - prepare_jinja2_compat diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_x509_certificate_info/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_certificate_info/tasks/impl.yml new file mode 100644 index 000000000..1923240a1 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_certificate_info/tasks/impl.yml @@ -0,0 +1,221 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Get certificate info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/cert_1.pem') | community.crypto.x509_certificate_info }} + result_idna: >- + {{ lookup('file', remote_tmp_dir ~ '/cert_1.pem') | community.crypto.x509_certificate_info(name_encoding='idna') }} + result_unicode: >- + {{ lookup('file', remote_tmp_dir ~ '/cert_1.pem') | community.crypto.x509_certificate_info(name_encoding='unicode') }} + +- name: Check whether issuer and subject and extensions behave as expected + assert: + that: + - result.issuer.organizationalUnitName == 'ACME Department' + - "['organizationalUnitName', 'Crypto Department'] in result.issuer_ordered" + - "['organizationalUnitName', 'ACME Department'] in result.issuer_ordered" + - result.subject.organizationalUnitName == 'ACME Department' + - "['organizationalUnitName', 'Crypto Department'] in result.subject_ordered" + - "['organizationalUnitName', 'ACME Department'] in result.subject_ordered" + - result.public_key_type == 'RSA' + - result.public_key_data.size == (default_rsa_key_size_certifiates | int) + - "result.subject_alt_name == [ + 'DNS:www.ansible.com', + 'DNS:' ~ ('öç' if cryptography_version.stdout is version('2.1', '<') else 'xn--7ca3a') ~ '.com', + 'DNS:' ~ ('www.öç' if cryptography_version.stdout is version('2.1', '<') else 'xn--74h') ~ '.com', + 'IP:1.2.3.4', + 'IP:::1', + 'email:test@example.org', + 'URI:https://example.org/test/index.html' + ]" + - "result_idna.subject_alt_name == [ + 'DNS:www.ansible.com', + 'DNS:xn--7ca3a.com', + 'DNS:' ~ ('www.xn--7ca3a' if cryptography_version.stdout is version('2.1', '<') else 'xn--74h') ~ '.com', + 'IP:1.2.3.4', + 'IP:::1', + 'email:test@example.org', + 'URI:https://example.org/test/index.html' + ]" + - "result_unicode.subject_alt_name == [ + 'DNS:www.ansible.com', + 'DNS:öç.com', + 'DNS:' ~ ('www.öç' if cryptography_version.stdout is version('2.1', '<') else '☺') ~ '.com', + 'IP:1.2.3.4', + 'IP:::1', + 'email:test@example.org', + 'URI:https://example.org/test/index.html' + ]" + # TLS Feature + - result.extensions_by_oid['1.3.6.1.5.5.7.1.24'].critical == false + - result.extensions_by_oid['1.3.6.1.5.5.7.1.24'].value == 'MAMCAQU=' + # Key Usage + - result.extensions_by_oid['2.5.29.15'].critical == true + - result.extensions_by_oid['2.5.29.15'].value in ['AwMA/4A=', 'AwMH/4A='] + # Subject Alternative Names + - result.extensions_by_oid['2.5.29.17'].critical == false + - > + result.extensions_by_oid['2.5.29.17'].value == ( + 'MIGCgg93d3cuYW5zaWJsZS5jb22CDXhuLS03Y2EzYS5jb22CEXd3dy54bi0tN2NhM2EuY29thwQBAgMEhxAAAAAAAAAAAAAAAAAAAAABgRB0ZXN0QGV4YW1wbGUub3JnhiNodHRwczovL2V4YW1wbGUub3JnL3Rlc3QvaW5kZXguaHRtbA==' + if cryptography_version.stdout is version('2.1', '<') else + 'MHyCD3d3dy5hbnNpYmxlLmNvbYINeG4tLTdjYTNhLmNvbYILeG4tLTc0aC5jb22HBAECAwSHEAAAAAAAAAAAAAAAAAAAAAGBEHRlc3RAZXhhbXBsZS5vcmeGI2h0dHBzOi8vZXhhbXBsZS5vcmcvdGVzdC9pbmRleC5odG1s' + ) + # Basic Constraints + - result.extensions_by_oid['2.5.29.19'].critical == true + - result.extensions_by_oid['2.5.29.19'].value == 'MAYBAf8CARc=' + # Extended Key Usage + - result.extensions_by_oid['2.5.29.37'].critical == false + - result.extensions_by_oid['2.5.29.37'].value == 'MHQGCCsGAQUFBwMBBggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUFBwMJBgRVHSUABggrBgEFBQcBAwYIKwYBBQUHAwoGCCsGAQUFBwMHBggrBgEFBQcBAg==' + +- name: Check SubjectKeyIdentifier and AuthorityKeyIdentifier + assert: + that: + - result.subject_key_identifier == "00:11:22:33" + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + # Subject Key Identifier + - result.extensions_by_oid['2.5.29.14'].critical == false + # Authority Key Identifier + - result.extensions_by_oid['2.5.29.35'].critical == false + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: cryptography_version.stdout is version('1.3', '>=') + +- name: Get certificate info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/cert_2.pem') | community.crypto.x509_certificate_info }} + +- name: Get certificate info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/cert_3.pem') | community.crypto.x509_certificate_info }} + +- name: Check AuthorityKeyIdentifier + assert: + that: + - result.authority_key_identifier is none + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: cryptography_version.stdout is version('1.3', '>=') + +- name: Get certificate info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/cert_4.pem') | community.crypto.x509_certificate_info }} + +- name: Check AuthorityKeyIdentifier + assert: + that: + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer is none + - result.authority_cert_serial_number is none + when: cryptography_version.stdout is version('1.3', '>=') + +- name: Get certificate info for packaged cert 1 + set_fact: + result: >- + {{ lookup('file', role_path ~ '/../x509_certificate_info/files/cert1.pem') | community.crypto.x509_certificate_info }} +- name: Check extensions + assert: + that: + - "'ocsp_uri' in result" + - "result.ocsp_uri == 'http://ocsp.int-x3.letsencrypt.org'" + - "'issuer_uri' in result" + - "result.issuer_uri == 'http://cert.int-x3.letsencrypt.org/'" + - result.extensions_by_oid | length == 9 + # Precert Signed Certificate Timestamps + - result.extensions_by_oid['1.3.6.1.4.1.11129.2.4.2'].critical == false + - result.extensions_by_oid['1.3.6.1.4.1.11129.2.4.2'].value == 'BIHyAPAAdgDBFkrgp3LS1DktyArBB3DU8MSb3pkaSEDB+gdRZPYzYAAAAWTdAoU6AAAEAwBHMEUCIG5WpfKF536KKa9fnVlYbwcfrKh09Hi2MSRwU2kad49UAiEA4RUKjJOgw11IHFNdit+sy1RcCU3QCSOEQYrJ1/oPltAAdgApPFGWVMg5ZbqqUPxYB9S3b79Yeily3KTDDPTlRUf0eAAAAWTdAoc+AAAEAwBHMEUCIQCJjo75K4rVDSiWQe3XFLY6MiG3zcHQrKb0YhM17r1UKAIgGa8qMoN03DLp+Rm9nRJ9XLbTJz1vbuu9PyXUY741P8E=' + # Authority Information Access + - result.extensions_by_oid['1.3.6.1.5.5.7.1.1'].critical == false + - result.extensions_by_oid['1.3.6.1.5.5.7.1.1'].value == 'MGEwLgYIKwYBBQUHMAGGImh0dHA6Ly9vY3NwLmludC14My5sZXRzZW5jcnlwdC5vcmcwLwYIKwYBBQUHMAKGI2h0dHA6Ly9jZXJ0LmludC14My5sZXRzZW5jcnlwdC5vcmcv' + # Subject Key Identifier + - result.extensions_by_oid['2.5.29.14'].critical == false + - result.extensions_by_oid['2.5.29.14'].value == 'BBRtcOI/yg62Ehbu5vQzxMUUdBOYMw==' + # Key Usage (The certificate has 'AwIFoA==', while de-serializing and re-serializing yields 'AwIAoA=='!) + - result.extensions_by_oid['2.5.29.15'].critical == true + - result.extensions_by_oid['2.5.29.15'].value in ['AwIFoA==', 'AwIAoA=='] + # Subject Alternative Names + - result.extensions_by_oid['2.5.29.17'].critical == false + - result.extensions_by_oid['2.5.29.17'].value == 'MIIB5IIbY2VydC5pbnQteDEubGV0c2VuY3J5cHQub3JnghtjZXJ0LmludC14Mi5sZXRzZW5jcnlwdC5vcmeCG2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9yZ4IbY2VydC5pbnQteDQubGV0c2VuY3J5cHQub3JnghxjZXJ0LnJvb3QteDEubGV0c2VuY3J5cHQub3Jngh9jZXJ0LnN0YWdpbmcteDEubGV0c2VuY3J5cHQub3Jngh9jZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JngiBjZXJ0LnN0Zy1yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ISY3AubGV0c2VuY3J5cHQub3JnghpjcC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ITY3BzLmxldHNlbmNyeXB0Lm9yZ4IbY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3Jnghtjcmwucm9vdC14MS5sZXRzZW5jcnlwdC5vcmeCD2xldHNlbmNyeXB0Lm9yZ4IWb3JpZ2luLmxldHNlbmNyeXB0Lm9yZ4IXb3JpZ2luMi5sZXRzZW5jcnlwdC5vcmeCFnN0YXR1cy5sZXRzZW5jcnlwdC5vcmeCE3d3dy5sZXRzZW5jcnlwdC5vcmc=' + # Basic Constraints + - result.extensions_by_oid['2.5.29.19'].critical == true + - result.extensions_by_oid['2.5.29.19'].value == 'MAA=' + # Certificate Policies + - result.extensions_by_oid['2.5.29.32'].critical == false + - result.extensions_by_oid['2.5.29.32'].value == 'MIHzMAgGBmeBDAECATCB5gYLKwYBBAGC3xMBAQEwgdYwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIGrBggrBgEFBQcCAjCBngyBm1RoaXMgQ2VydGlmaWNhdGUgbWF5IG9ubHkgYmUgcmVsaWVkIHVwb24gYnkgUmVseWluZyBQYXJ0aWVzIGFuZCBvbmx5IGluIGFjY29yZGFuY2Ugd2l0aCB0aGUgQ2VydGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vbGV0c2VuY3J5cHQub3JnL3JlcG9zaXRvcnkv' + # Authority Key Identifier + - result.extensions_by_oid['2.5.29.35'].critical == false + - result.extensions_by_oid['2.5.29.35'].value == 'MBaAFKhKamMEfd265tE5t6ZFZe/zqOyh' + # Extended Key Usage + - result.extensions_by_oid['2.5.29.37'].critical == false + - result.extensions_by_oid['2.5.29.37'].value == 'MBQGCCsGAQUFBwMBBggrBgEFBQcDAg==' +- name: Check fingerprints + assert: + that: + - (result.fingerprints.sha256 == '57:7c:f1:f5:dd:cc:6e:e9:f3:17:28:73:17:e4:25:c7:69:74:3e:f7:9a:df:58:20:7a:5a:e4:aa:de:bf:24:5b' if result.fingerprints.sha256 is defined else true) + - (result.fingerprints.sha1 == 'b7:79:64:f4:2b:e0:ae:45:74:d4:f3:08:f6:53:cb:39:26:fa:52:6b' if result.fingerprints.sha1 is defined else true) + +- name: Get invalid certificate info + set_fact: + result: >- + {{ [] | community.crypto.x509_certificate_info }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^The community.crypto.x509_certificate_info input must be a text type, not <(?:class|type) 'list'>$") + +- name: Get invalid certificate info + set_fact: + result: >- + {{ 'foo' | community.crypto.x509_certificate_info }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^Unable to load (?:certificate|PEM file)(?:\.|$)") + +- name: Get invalid certificate info + set_fact: + result: >- + {{ 'foo' | community.crypto.x509_certificate_info(name_encoding=[]) }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^The name_encoding option must be of a text type, not <(?:class|type) 'list'>$") + +- name: Get invalid name_encoding parameter + set_fact: + result: >- + {{ 'bar' | community.crypto.x509_certificate_info(name_encoding='foo') }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^The name_encoding option must be one of the values \"ignore\", \"idna\", or \"unicode\", not \"foo\"$") diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_x509_certificate_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_certificate_info/tasks/main.yml new file mode 100644 index 000000000..37b1fccda --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_certificate_info/tasks/main.yml @@ -0,0 +1,151 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Make sure the Python idna library is installed + pip: + name: idna + state: present + +- name: Generate privatekey + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey.pem' + size: '{{ default_rsa_key_size_certifiates }}' + +- name: Generate privatekey with password + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + size: '{{ default_rsa_key_size_certifiates }}' + +- name: Generate CSR 1 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_1.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.example.com + C: de + L: Somewhere + ST: Zurich + streetAddress: Welcome Street + O: Ansible + organizationalUnitName: + - Crypto Department + - ACME Department + serialNumber: "1234" + SN: Last Name + GN: First Name + title: Chief + pseudonym: test + UID: asdf + emailAddress: test@example.com + postalAddress: 1234 Somewhere + postalCode: "1234" + useCommonNameForSAN: false + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: true + extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + subject_alt_name: + - "DNS:www.ansible.com" + - "DNS:öç.com" + # cryptography < 2.1 cannot handle certain Unicode characters + - "DNS:{{ 'www.öç' if cryptography_version.stdout is version('2.1', '<') else '☺' }}.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: true + ocsp_must_staple: true + subject_key_identifier: '{{ "00:11:22:33" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + +- name: Generate CSR 2 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_2.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + privatekey_passphrase: hunter2 + useCommonNameForSAN: false + basic_constraints: + - "CA:TRUE" + +- name: Generate CSR 3 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_3.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + useCommonNameForSAN: false + subject_alt_name: + - "DNS:*.ansible.com" + - "DNS:*.example.org" + - "IP:DEAD:BEEF::1" + basic_constraints: + - "CA:FALSE" + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + +- name: Generate CSR 4 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_4.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + useCommonNameForSAN: false + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + +- name: Generate selfsigned certificates + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_{{ item }}.pem' + csr_path: '{{ remote_tmp_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_not_after: "+10d" + selfsigned_not_before: "-3d" + loop: + - 1 + - 2 + - 3 + - 4 + +- name: Running tests + include_tasks: impl.yml + when: cryptography_version.stdout is version('1.6', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_x509_crl_info/aliases b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_crl_info/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_crl_info/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_x509_crl_info/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_crl_info/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_crl_info/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_x509_crl_info/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_crl_info/tasks/impl.yml new file mode 100644 index 000000000..4f2412d24 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_crl_info/tasks/impl.yml @@ -0,0 +1,346 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create CRL 1 + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ remote_tmp_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + +- name: Retrieve CRL 1 infos + set_fact: + crl_1_info_1: >- + {{ lookup('file', remote_tmp_dir ~ '/ca-crl1.crl') | community.crypto.x509_crl_info }} + +- name: Retrieve CRL 1 infos + set_fact: + crl_1_info_2: >- + {{ lookup('file', remote_tmp_dir ~ '/ca-crl1.crl') | b64encode | community.crypto.x509_crl_info }} + +- name: Validate CRL 1 info + assert: + that: + - crl_1_info_1.format == 'pem' + - crl_1_info_1.digest == 'ecdsa-with-SHA256' + - crl_1_info_1.issuer | length == 1 + - crl_1_info_1.issuer.commonName == 'Ansible' + - crl_1_info_1.issuer_ordered | length == 1 + - crl_1_info_1.last_update == '20191013000000Z' + - crl_1_info_1.next_update == '20191113000000Z' + - crl_1_info_1.revoked_certificates | length == 3 + - crl_1_info_1.revoked_certificates[0].invalidity_date is none + - crl_1_info_1.revoked_certificates[0].invalidity_date_critical == false + - crl_1_info_1.revoked_certificates[0].issuer is none + - crl_1_info_1.revoked_certificates[0].issuer_critical == false + - crl_1_info_1.revoked_certificates[0].reason is none + - crl_1_info_1.revoked_certificates[0].reason_critical == false + - crl_1_info_1.revoked_certificates[0].revocation_date == '20191013000000Z' + - crl_1_info_1.revoked_certificates[0].serial_number == certificate_infos.results[0].serial_number + - crl_1_info_1.revoked_certificates[1].invalidity_date == '20191012000000Z' + - crl_1_info_1.revoked_certificates[1].invalidity_date_critical == false + - crl_1_info_1.revoked_certificates[1].issuer is none + - crl_1_info_1.revoked_certificates[1].issuer_critical == false + - crl_1_info_1.revoked_certificates[1].reason == 'key_compromise' + - crl_1_info_1.revoked_certificates[1].reason_critical == true + - crl_1_info_1.revoked_certificates[1].revocation_date == '20191013000000Z' + - crl_1_info_1.revoked_certificates[1].serial_number == certificate_infos.results[1].serial_number + - crl_1_info_1.revoked_certificates[2].invalidity_date is none + - crl_1_info_1.revoked_certificates[2].invalidity_date_critical == false + - crl_1_info_1.revoked_certificates[2].issuer is none + - crl_1_info_1.revoked_certificates[2].issuer_critical == false + - crl_1_info_1.revoked_certificates[2].reason is none + - crl_1_info_1.revoked_certificates[2].reason_critical == false + - crl_1_info_1.revoked_certificates[2].revocation_date == '20191001000000Z' + - crl_1_info_1.revoked_certificates[2].serial_number == 1234 + - crl_1_info_1 == crl_1_info_2 + +- name: Recreate CRL 1 as DER file + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + format: der + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ remote_tmp_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + +- name: Read ca-crl1.crl + slurp: + src: "{{ remote_tmp_dir }}/ca-crl1.crl" + register: content + +- name: Retrieve CRL 1 infos from DER (raw bytes) + set_fact: + crl_1_info_4: >- + {{ content.content | b64decode | community.crypto.x509_crl_info }} + # Ansible 2.9 and ansible-base 2.10 on Python 2 mangle bytes, so do not run this on these versions + when: ansible_version.string is version('2.11', '>=') or ansible_python.version.major > 2 + +- name: Retrieve CRL 1 infos from DER (Base64 encoded) + set_fact: + crl_1_info_5: >- + {{ content.content | community.crypto.x509_crl_info }} + +- name: Validate CRL 1 + assert: + that: + - crl_1_info_4 is not defined or crl_1_info_4.format == 'der' + - crl_1_info_5.format == 'der' + - crl_1_info_4 is not defined or crl_1_info_4 == crl_1_info_5 + +- name: Create CRL 2 + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - CN: CRL + - countryName: US + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + ignore_timestamps: false + crl_mode: update + return_content: true + register: crl_2_change + +- name: Retrieve CRL 2 infos + set_fact: + crl_2_info_1: >- + {{ lookup('file', remote_tmp_dir ~ '/ca-crl2.crl') | community.crypto.x509_crl_info(list_revoked_certificates=false) }} + +- name: Create CRL 2 (changed order) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - countryName: US + - CN: CRL + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + ignore_timestamps: true + crl_mode: update + return_content: true + register: crl_2_change_order + +- name: Retrieve CRL 2 infos again + set_fact: + crl_2_info_2: >- + {{ lookup('file', remote_tmp_dir ~ '/ca-crl2.crl') | community.crypto.x509_crl_info(list_revoked_certificates=false) }} + +- name: Validate CRL 2 info + assert: + that: + - "'revoked_certificates' not in crl_2_info_1" + - > + crl_2_info_1.issuer_ordered == [ + ['commonName', 'Ansible'], + ['commonName', 'CRL'], + ['countryName', 'US'], + ['commonName', 'Test'], + ] + - > + crl_2_info_2.issuer_ordered == [ + ['commonName', 'Ansible'], + ['countryName', 'US'], + ['commonName', 'CRL'], + ['commonName', 'Test'], + ] + +- name: Create CRL 3 + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl3.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - serial_number: 1234 + revocation_date: 20191001000000Z + # * cryptography < 2.1 strips username and password from URIs. To avoid problems, we do + # not pass usernames and passwords for URIs when the cryptography version is < 2.1. + # * Python 3.5 before 3.5.8 rc 1 has a bug in urllib.parse.urlparse() that results in an + # error if a Unicode netloc has a username or password included. + # (https://github.com/ansible-collections/community.crypto/pull/436#issuecomment-1101737134) + # This affects the Python 3.5 included in Ansible 2.9's default test container; to avoid + # this, we also do not pass usernames and passwords for Python 3.5. + issuer: + - "DNS:ca.example.org" + - "DNS:ffóò.ḃâŗ.çøṁ" + - "email:foo@ḃâŗ.çøṁ" + - "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'admin:hunter2@' }}ffóò.ḃâŗ.çøṁ/baz?foo=bar" + - "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'goo@' }}www.straße.de" + - "URI:https://straße.de:8080" + - "URI:http://gefäß.org" + - "URI:http://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'a:b@' }}ä:1" + issuer_critical: true + register: crl_3 + +- name: Create CRL 3 (IDNA encoding) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl3.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - serial_number: 1234 + revocation_date: 20191001000000Z + issuer: + - "DNS:ca.example.org" + - "DNS:xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n" + - "email:foo@xn--2ca8uh37e.xn--7ca8a981n" + - "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'admin:hunter2@' }}xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n/baz?foo=bar" + - "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'goo@' }}www.xn--strae-oqa.de" + - "URI:https://xn--strae-oqa.de:8080" + - "URI:http://xn--gef-7kay.org" + - "URI:http://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'a:b@' }}xn--4ca:1" + issuer_critical: true + ignore_timestamps: true + name_encoding: idna + register: crl_3_idna + +- name: Create CRL 3 (Unicode encoding) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl3.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - serial_number: 1234 + revocation_date: 20191001000000Z + issuer: + - "DNS:ca.example.org" + - "DNS:ffóò.ḃâŗ.çøṁ" + - "email:foo@ḃâŗ.çøṁ" + - "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'admin:hunter2@' }}ffóò.ḃâŗ.çøṁ/baz?foo=bar" + - "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'goo@' }}www.straße.de" + - "URI:https://straße.de:8080" + - "URI:http://gefäß.org" + - "URI:http://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'a:b@' }}ä:1" + issuer_critical: true + ignore_timestamps: true + name_encoding: unicode + register: crl_3_unicode + +- name: Retrieve CRL 3 infos + set_fact: + crl_3_info: >- + {{ lookup('file', remote_tmp_dir ~ '/ca-crl3.crl') | community.crypto.x509_crl_info(list_revoked_certificates=true) }} + crl_3_info_idna: >- + {{ lookup('file', remote_tmp_dir ~ '/ca-crl3.crl') | community.crypto.x509_crl_info(list_revoked_certificates=true, name_encoding='idna') }} + crl_3_info_unicode: >- + {{ lookup('file', remote_tmp_dir ~ '/ca-crl3.crl') | community.crypto.x509_crl_info(list_revoked_certificates=true, name_encoding='unicode') }} + +- name: Validate CRL 3 info + assert: + that: + - crl_3.revoked_certificates == crl_3_info.revoked_certificates + - crl_3_idna.revoked_certificates == crl_3_info_idna.revoked_certificates + - crl_3_unicode.revoked_certificates == crl_3_info_unicode.revoked_certificates + +- name: Get invalid CRL info + set_fact: + result: >- + {{ [] | community.crypto.x509_crl_info }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^The community.crypto.x509_crl_info input must be a text type, not <(?:class|type) 'list'>$") + +- name: Get invalid CRL info + set_fact: + result: >- + {{ 'foo' | community.crypto.x509_crl_info }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^Error while decoding CRL") + +- name: Get invalid CRL info + set_fact: + result: >- + {{ 'foo' | community.crypto.x509_crl_info(name_encoding=[]) }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^The name_encoding option must be of a text type, not <(?:class|type) 'list'>$") + +- name: Get invalid name_encoding parameter + set_fact: + result: >- + {{ 'bar' | community.crypto.x509_crl_info(name_encoding='foo') }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^The name_encoding option must be one of the values \"ignore\", \"idna\", or \"unicode\", not \"foo\"$") + +- name: Get invalid list_revoked_certificates parameter + set_fact: + result: >- + {{ 'bar' | community.crypto.x509_crl_info(list_revoked_certificates=[]) }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^The list_revoked_certificates option must be a boolean, not <(?:class|type) 'list'>$") diff --git a/ansible_collections/community/crypto/tests/integration/targets/filter_x509_crl_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_crl_info/tasks/main.yml new file mode 100644 index 000000000..0270b07d2 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/filter_x509_crl_info/tasks/main.yml @@ -0,0 +1,91 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Make sure the Python idna library is installed + pip: + name: idna + state: present + +- set_fact: + certificates: + - name: ca + subject: + commonName: Ansible + is_ca: true + - name: ca-2 + subject: + commonName: Ansible Other CA + is_ca: true + - 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: '{{ remote_tmp_dir }}/{{ item.name }}.key' + type: ECC + curve: secp256r1 + loop: "{{ certificates }}" + +- name: Generate CSRs + openssl_csr: + path: '{{ remote_tmp_dir }}/{{ item.name }}.csr' + privatekey_path: '{{ remote_tmp_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: false + loop: "{{ certificates }}" + +- name: Generate CA certificates + x509_certificate: + path: '{{ remote_tmp_dir }}/{{ item.name }}.pem' + csr_path: '{{ remote_tmp_dir }}/{{ item.name }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/{{ item.name }}.key' + provider: selfsigned + loop: "{{ certificates }}" + when: item.is_ca | default(false) + +- name: Generate other certificates + x509_certificate: + path: '{{ remote_tmp_dir }}/{{ item.name }}.pem' + csr_path: '{{ remote_tmp_dir }}/{{ item.name }}.csr' + provider: ownca + ownca_path: '{{ remote_tmp_dir }}/ca.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca.key' + loop: "{{ certificates }}" + when: not (item.is_ca | default(false)) + +- name: Get certificate infos + x509_certificate_info: + path: '{{ remote_tmp_dir }}/{{ item }}.pem' + loop: + - cert-1 + - cert-2 + - cert-3 + - cert-4 + register: certificate_infos + +- block: + - name: Running tests + include_tasks: impl.yml + + when: cryptography_version.stdout is version('1.2', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/get_certificate/aliases b/ansible_collections/community/crypto/tests/integration/targets/get_certificate/aliases new file mode 100644 index 000000000..040a5b947 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/get_certificate/aliases @@ -0,0 +1,11 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/1 +azp/posix/1 +destructive +needs/httptester + +# For some reason connecting to helper containers does not work on the Alpine VMs +skip/alpine diff --git a/ansible_collections/community/crypto/tests/integration/targets/get_certificate/files/process_certs.py b/ansible_collections/community/crypto/tests/integration/targets/get_certificate/files/process_certs.py new file mode 100644 index 000000000..639104318 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/get_certificate/files/process_certs.py @@ -0,0 +1,32 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +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) diff --git a/ansible_collections/community/crypto/tests/integration/targets/get_certificate/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/get_certificate/meta/main.yml new file mode 100644 index 000000000..a5f4dfb0f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/get_certificate/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir + - prepare_http_tests diff --git a/ansible_collections/community/crypto/tests/integration/targets/get_certificate/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/get_certificate/tasks/main.yml new file mode 100644 index 000000000..cd5b93979 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/get_certificate/tasks/main.yml @@ -0,0 +1,48 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- set_fact: + skip_tests: false + +- block: + + - name: Get servers certificate with backend auto-detection + get_certificate: + host: "{{ httpbin_host }}" + port: 443 + ignore_errors: true + register: result + + - set_fact: + skip_tests: | + {{ + result is failed and ( + 'error: [Errno 1] _ssl.c:492: error:14094410:SSL routines:SSL3_READ_BYTES:sslv3 alert handshake failure' in result.msg + or + 'error: _ssl.c:314: Invalid SSL protocol variant specified.' in result.msg + ) + }} + + - assert: + that: + - result is success or skip_tests + + when: cryptography_version.stdout is version('1.6', '>=') + +- 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 not skip_tests diff --git a/ansible_collections/community/crypto/tests/integration/targets/get_certificate/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/get_certificate/tests/validate.yml new file mode 100644 index 000000000..810a66f85 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/get_certificate/tests/validate.yml @@ -0,0 +1,167 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Get servers certificate for SNI test part 1 + get_certificate: + host: "{{ httpbin_host }}" + port: 443 + server_name: "{{ sni_host }}" + asn1_base64: true + 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 + - "'{{ sni_host }}' == result.subject.CN" + +- name: Get servers certificate for SNI test part 2 + get_certificate: + host: "{{ sni_host }}" + port: 443 + server_name: "{{ httpbin_host }}" + asn1_base64: true + 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 + - "'{{ httpbin_host }}' == result.subject.CN" + +- name: Get servers certificate + get_certificate: + host: "{{ httpbin_host }}" + port: 443 + select_crypto_backend: "{{ select_crypto_backend }}" + asn1_base64: true + 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 }}" + asn1_base64: true + 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 }}" + asn1_base64: true + 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 }}" + asn1_base64: true + 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: "{{ remote_tmp_dir }}/temp.pem" + +- name: Get servers certificate comparing it to its own ca_cert file + get_certificate: + ca_cert: '{{ remote_tmp_dir }}/temp.pem' + host: "{{ httpbin_host }}" + port: 443 + select_crypto_backend: "{{ select_crypto_backend }}" + asn1_base64: true + register: result + +- assert: + that: + - result is not changed + - result is not failed + +- name: Generate bogus CA privatekey + openssl_privatekey: + path: '{{ remote_tmp_dir }}/bogus_ca.key' + type: ECC + curve: secp256r1 + +- name: Generate bogus CA CSR + openssl_csr: + path: '{{ remote_tmp_dir }}/bogus_ca.csr' + privatekey_path: '{{ remote_tmp_dir }}/bogus_ca.key' + subject: + commonName: Bogus CA + useCommonNameForSAN: false + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: true + +- name: Generate selfsigned bogus CA certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/bogus_ca.pem' + csr_path: '{{ remote_tmp_dir }}/bogus_ca.csr' + privatekey_path: '{{ remote_tmp_dir }}/bogus_ca.key' + provider: selfsigned + selfsigned_digest: sha256 + +- name: Get servers certificate comparing it to an invalid ca_cert file + get_certificate: + ca_cert: '{{ remote_tmp_dir }}/bogus_ca.pem' + host: "{{ httpbin_host }}" + port: 443 + select_crypto_backend: "{{ select_crypto_backend }}" + asn1_base64: true + register: result + ignore_errors: true + +- assert: + that: + - result is not changed + - result is failed diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/aliases b/ansible_collections/community/crypto/tests/integration/targets/luks_device/aliases new file mode 100644 index 000000000..b6057ff6b --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/aliases @@ -0,0 +1,12 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/2 +azp/posix/vm +skip/osx +skip/macos +skip/freebsd +skip/docker +needs/root +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/files/keyfile1 b/ansible_collections/community/crypto/tests/integration/targets/luks_device/files/keyfile1 new file mode 100644 index 000000000..5e40c0877 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/files/keyfile1 @@ -0,0 +1 @@ +asdf
\ No newline at end of file diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/files/keyfile1.license b/ansible_collections/community/crypto/tests/integration/targets/luks_device/files/keyfile1.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/files/keyfile1.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/files/keyfile2 b/ansible_collections/community/crypto/tests/integration/targets/luks_device/files/keyfile2 new file mode 100644 index 000000000..5e4f25651 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/files/keyfile2 @@ -0,0 +1 @@ +test1234
\ No newline at end of file diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/files/keyfile2.license b/ansible_collections/community/crypto/tests/integration/targets/luks_device/files/keyfile2.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/files/keyfile2.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/luks_device/meta/main.yml new file mode 100644 index 000000000..982de6eb0 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/meta/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/main.yml new file mode 100644 index 000000000..2570fa311 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/main.yml @@ -0,0 +1,91 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Copy keyfiles + copy: + src: '{{ item }}' + dest: '{{ remote_tmp_dir }}/{{ item }}' + loop: + - keyfile1 + - keyfile2 + +- name: Include OS-specific variables + include_vars: '{{ lookup("first_found", search) }}' + vars: + search: + files: + - '{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml' + - '{{ ansible_distribution }}-{{ ansible_distribution_version }}.yml' + - '{{ ansible_distribution }}.yml' + - '{{ ansible_os_family }}.yml' + - default.yml + paths: + - vars + +- name: Make sure cryptsetup is installed + package: + name: cryptsetup + state: present + become: true + +- name: Install additionally required packages + package: + name: '{{ luks_extra_packages }}' + state: present + become: true + when: luks_extra_packages | length > 0 + +- name: Determine cryptsetup version + command: cryptsetup --version + register: cryptsetup_version + +- name: Extract cryptsetup version + set_fact: + cryptsetup_version: >- + {{ cryptsetup_version.stdout_lines[0] | regex_search('cryptsetup ([0-9]+\.[0-9]+\.[0-9]+)') | split | last }} + +- name: Create cryptfile + command: dd if=/dev/zero of={{ remote_tmp_dir.replace('~', ansible_env.HOME) }}/cryptfile bs=1M count=32 + +- name: Figure out next loopback device + command: losetup -f + become: true + register: cryptfile_device_output + +- name: Create lookback device + command: losetup -f {{ remote_tmp_dir.replace('~', ansible_env.HOME) }}/cryptfile + become: true + +- name: Store some common data for tests + set_fact: + cryptfile_device: "{{ cryptfile_device_output.stdout_lines[0] }}" + cryptfile_passphrase1: "uNiJ9vKG2mUOEWDiQVuBHJlfMHE" + cryptfile_passphrase2: "HW4Ak2HtE2vvne0qjJMPTtmbV4M" + cryptfile_passphrase3: "qQJqsjabO9pItV792k90VvX84MM" + +- block: + - include_tasks: run-test.yml + with_fileglob: + - "tests/*.yml" + + always: + - name: Make sure LUKS device is gone + luks_device: + device: "{{ cryptfile_device }}" + state: absent + become: true + ignore_errors: true + + - command: losetup -d "{{ cryptfile_device }}" + become: true + + - file: + dest: "{{ remote_tmp_dir.replace('~', ansible_env.HOME) }}/cryptfile" + state: absent diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/run-test.yml b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/run-test.yml new file mode 100644 index 000000000..eff7ac731 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/run-test.yml @@ -0,0 +1,12 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Make sure LUKS device is gone + luks_device: + device: "{{ cryptfile_device }}" + state: absent + become: true +- name: "Loading tasks from {{ item }}" + include_tasks: "{{ item }}" diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/create-destroy.yml b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/create-destroy.yml new file mode 100644 index 000000000..7210b9e3f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/create-destroy.yml @@ -0,0 +1,199 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create (check) + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + check_mode: true + become: true + register: create_check +- name: Create + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + become: true + register: create +- name: Create (idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + become: true + register: create_idem +- name: Create (idempotent, check) + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + check_mode: true + become: true + register: create_idem_check +- assert: + that: + - create_check is changed + - create is changed + - create_idem is not changed + - create_idem_check is not changed + +- name: Open (check) + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + check_mode: true + become: true + register: open_check +- name: Open + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + become: true + register: open +- name: Open (idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + become: true + register: open_idem +- name: Open (idempotent, check) + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + check_mode: true + become: true + register: open_idem_check +- assert: + that: + - open_check is changed + - open is changed + - open_idem is not changed + - open_idem_check is not changed + +- name: Closed (via name, check) + luks_device: + name: "{{ open.name }}" + state: closed + check_mode: true + become: true + register: close_check +- name: Closed (via name) + luks_device: + name: "{{ open.name }}" + state: closed + become: true + register: close +- name: Closed (via name, idempotent) + luks_device: + name: "{{ open.name }}" + state: closed + become: true + register: close_idem +- name: Closed (via name, idempotent, check) + luks_device: + name: "{{ open.name }}" + state: closed + check_mode: true + become: true + register: close_idem_check +- assert: + that: + - close_check is changed + - close is changed + - close_idem is not changed + - close_idem_check is not changed + +- name: Re-open + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + become: true + +- name: Closed (via device, check) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + check_mode: true + become: true + register: close_check +- name: Closed (via device) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: true + register: close +- name: Closed (via device, idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: true + register: close_idem +- name: Closed (via device, idempotent, check) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + check_mode: true + become: true + register: close_idem_check +- assert: + that: + - close_check is changed + - close is changed + - close_idem is not changed + - close_idem_check is not changed + +- name: Re-opened + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + become: true + +- name: Absent (check) + luks_device: + device: "{{ cryptfile_device }}" + state: absent + check_mode: true + become: true + register: absent_check +- name: Absent + luks_device: + device: "{{ cryptfile_device }}" + state: absent + become: true + register: absent +- name: Absent (idempotence) + luks_device: + device: "{{ cryptfile_device }}" + state: absent + become: true + register: absent_idem +- name: Absent (idempotence, check) + luks_device: + device: "{{ cryptfile_device }}" + state: absent + check_mode: true + become: true + register: absent_idem_check +- assert: + that: + - absent_check is changed + - absent is changed + - absent_idem is not changed + - absent_idem_check is not changed diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/device-check.yml b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/device-check.yml new file mode 100644 index 000000000..e6f8a6a12 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/device-check.yml @@ -0,0 +1,60 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create with invalid device name (check) + luks_device: + device: /dev/asdfasdfasdf + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + check_mode: true + ignore_errors: true + become: true + register: create_check +- name: Create with invalid device name + luks_device: + device: /dev/asdfasdfasdf + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + ignore_errors: true + become: true + register: create +- assert: + that: + - create_check is failed + - create is failed + - "'o such file or directory' in create_check.msg" + - "'o such file or directory' in create.msg" + +- name: Create with something which is not a device (check) + luks_device: + device: /tmp/ + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + check_mode: true + ignore_errors: true + become: true + register: create_check +- name: Create with something which is not a device + luks_device: + device: /tmp/ + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + ignore_errors: true + become: true + register: create +- assert: + that: + - create_check is failed + - create is failed + - "'is not a device' in create_check.msg" + - "'is not a device' in create.msg" diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/key-management.yml b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/key-management.yml new file mode 100644 index 000000000..302509de6 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/key-management.yml @@ -0,0 +1,206 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create with keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + become: true + +# Access: keyfile1 + +- name: Try to open with keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: true + +- name: Try to open with keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile2" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is failed + +- name: Give access to keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ remote_tmp_dir }}/keyfile1" + new_keyfile: "{{ remote_tmp_dir }}/keyfile2" + pbkdf: + iteration_time: 0.1 + become: true + register: result_1 + +- name: Give access to keyfile2 (idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ remote_tmp_dir }}/keyfile1" + new_keyfile: "{{ remote_tmp_dir }}/keyfile2" + become: true + register: result_2 + +- assert: + that: + - result_1 is changed + - result_2 is not changed + +# Access: keyfile1 and keyfile2 + +- name: Try to open with keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile2" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: true + +- name: Dump LUKS header + command: "cryptsetup luksDump {{ cryptfile_device }}" + become: true + +- name: Remove access from keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ remote_tmp_dir }}/keyfile1" + remove_keyfile: "{{ remote_tmp_dir }}/keyfile1" + become: true + register: result_1 + +- name: Remove access from keyfile1 (idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ remote_tmp_dir }}/keyfile1" + remove_keyfile: "{{ remote_tmp_dir }}/keyfile1" + become: true + register: result_2 + +- assert: + that: + - result_1 is changed + - result_2 is not changed + +# Access: keyfile2 + +- name: Try to open with keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is failed + +- name: Try to open with keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile2" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: true + +- name: Dump LUKS header + command: "cryptsetup luksDump {{ cryptfile_device }}" + become: true + +- name: Remove access from keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ remote_tmp_dir }}/keyfile2" + remove_keyfile: "{{ remote_tmp_dir }}/keyfile2" + become: true + ignore_errors: true + register: remove_last_key +- assert: + that: + - remove_last_key is failed + - "'force_remove_last_key' in remove_last_key.msg" + +# Access: keyfile2 + +- name: Try to open with keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile2" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: true + +- name: Remove access from keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ remote_tmp_dir }}/keyfile2" + remove_keyfile: "{{ remote_tmp_dir }}/keyfile2" + force_remove_last_key: true + become: true + +# Access: none + +- name: Try to open with keyfile2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile2" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is failed diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/options.yml b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/options.yml new file mode 100644 index 000000000..64df09515 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/options.yml @@ -0,0 +1,57 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create with keysize + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + keysize: 256 + pbkdf: + algorithm: pbkdf2 + iteration_count: 1000 + become: true + register: create_with_keysize +- name: Create with keysize (idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + keysize: 256 + pbkdf: + algorithm: pbkdf2 + iteration_count: 1000 + become: true + register: create_idem_with_keysize +- name: Create with different keysize (idempotent since we do not update keysize) + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + keysize: 512 + pbkdf: + algorithm: pbkdf2 + iteration_count: 1000 + become: true + register: create_idem_with_diff_keysize +- name: Create with ambiguous arguments + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + passphrase: "{{ cryptfile_passphrase1 }}" + pbkdf: + algorithm: pbkdf2 + iteration_count: 1000 + ignore_errors: true + become: true + register: create_with_ambiguous + +- assert: + that: + - create_with_keysize is changed + - create_idem_with_keysize is not changed + - create_idem_with_diff_keysize is not changed + - create_with_ambiguous is failed diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/passphrase.yml b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/passphrase.yml new file mode 100644 index 000000000..19551eccd --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/passphrase.yml @@ -0,0 +1,247 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create with passphrase1 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + passphrase: "{{ cryptfile_passphrase1 }}" + type: luks2 + pbkdf: + iteration_time: 0.1 + algorithm: argon2i + memory: 1000 + parallel: 1 + sector_size: 1024 + become: true + ignore_errors: true + register: create_passphrase_1 + +- name: Make sure that the previous task only fails if LUKS2 is not supported + assert: + that: + - "'Unknown option --type' in create_passphrase_1.msg" + when: create_passphrase_1 is failed + +- name: Create with passphrase1 (without argon2i) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + passphrase: "{{ cryptfile_passphrase1 }}" + pbkdf: + iteration_time: 0.1 + become: true + when: create_passphrase_1 is failed + +- name: Open with passphrase1 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + passphrase: "{{ cryptfile_passphrase1 }}" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: true + +- name: Give access with ambiguous new_ arguments + luks_device: + device: "{{ cryptfile_device }}" + state: closed + passphrase: "{{ cryptfile_passphrase1 }}" + new_passphrase: "{{ cryptfile_passphrase2 }}" + new_keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + become: true + ignore_errors: true + register: new_try +- assert: + that: + - new_try is failed + +- name: Try to open with passphrase2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + passphrase: "{{ cryptfile_passphrase2 }}" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is failed + +- name: Give access to passphrase2 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + passphrase: "{{ cryptfile_passphrase1 }}" + new_passphrase: "{{ cryptfile_passphrase2 }}" + pbkdf: + iteration_time: 0.1 + become: true + register: result_1 + +- name: Give access to passphrase2 (idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + passphrase: "{{ cryptfile_passphrase1 }}" + new_passphrase: "{{ cryptfile_passphrase2 }}" + become: true + register: result_2 + +- assert: + that: + - result_1 is changed + - result_2 is not changed + +- name: Open with passphrase2 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + passphrase: "{{ cryptfile_passphrase2 }}" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: true + +- name: Try to open with keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is failed + +- name: Give access to keyfile1 from passphrase1 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + passphrase: "{{ cryptfile_passphrase1 }}" + new_keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + become: true + +- name: Remove access with ambiguous remove_ arguments + luks_device: + device: "{{ cryptfile_device }}" + state: closed + remove_keyfile: "{{ remote_tmp_dir }}/keyfile1" + remove_passphrase: "{{ cryptfile_passphrase1 }}" + become: true + ignore_errors: true + register: remove_try +- assert: + that: + - remove_try is failed + +- name: Open with keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: true + +- name: Remove access for passphrase1 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + remove_passphrase: "{{ cryptfile_passphrase1 }}" + become: true + register: result_1 + +- name: Remove access for passphrase1 (idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: closed + remove_passphrase: "{{ cryptfile_passphrase1 }}" + become: true + register: result_2 + +- assert: + that: + - result_1 is changed + - result_2 is not changed + +- name: Try to open with passphrase1 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + passphrase: "{{ cryptfile_passphrase1 }}" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is failed + +- name: Try to open with passphrase3 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + passphrase: "{{ cryptfile_passphrase3 }}" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is failed + +- name: Give access to passphrase3 from keyfile1 + luks_device: + device: "{{ cryptfile_device }}" + state: closed + keyfile: "{{ remote_tmp_dir }}/keyfile1" + new_passphrase: "{{ cryptfile_passphrase3 }}" + pbkdf: + iteration_time: 0.1 + become: true + +- name: Open with passphrase3 + luks_device: + device: "{{ cryptfile_device }}" + state: opened + passphrase: "{{ cryptfile_passphrase3 }}" + become: true + ignore_errors: true + register: open_try +- assert: + that: + - open_try is not failed +- name: Close + luks_device: + device: "{{ cryptfile_device }}" + state: closed + become: true diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/performance.yml b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/performance.yml new file mode 100644 index 000000000..572625517 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/tasks/tests/performance.yml @@ -0,0 +1,103 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: On kernel >= 5.9 use performance flags + block: + - name: Create and open (check) + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + perf_same_cpu_crypt: true + perf_submit_from_crypt_cpus: true + perf_no_read_workqueue: true + perf_no_write_workqueue: true + persistent: true + pbkdf: + iteration_time: 0.1 + check_mode: true + become: true + register: create_open_check + - name: Create and open + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + perf_same_cpu_crypt: true + perf_submit_from_crypt_cpus: true + perf_no_read_workqueue: true + perf_no_write_workqueue: true + persistent: true + become: true + register: create_open + - name: Create and open (idempotent) + luks_device: + device: "{{ cryptfile_device }}" + state: opened + keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + perf_same_cpu_crypt: true + perf_submit_from_crypt_cpus: true + perf_no_read_workqueue: true + perf_no_write_workqueue: true + persistent: true + become: true + register: create_open_idem + - name: Create and open (idempotent, check) + luks_device: + device: "{{ cryptfile_device }}" + state: present + keyfile: "{{ remote_tmp_dir }}/keyfile1" + pbkdf: + iteration_time: 0.1 + perf_same_cpu_crypt: true + perf_submit_from_crypt_cpus: true + perf_no_read_workqueue: true + perf_no_write_workqueue: true + persistent: true + check_mode: true + become: true + register: create_open_idem_check + - assert: + that: + - create_open_check is changed + - create_open is changed + - create_open_idem is not changed + - create_open_idem_check is not changed + + - name: Dump LUKS Header + command: "cryptsetup luksDump {{ cryptfile_device }}" + become: true + register: luks_header + - assert: + that: + - "'no-read-workqueue' in luks_header.stdout" + - "'no-write-workqueue' in luks_header.stdout" + - "'same-cpu-crypt' in luks_header.stdout" + - "'submit-from-crypt-cpus' in luks_header.stdout" + + - name: Dump device mapper table + command: "dmsetup table {{ create_open.name }}" + become: true + register: dm_table + - assert: + that: + - "'no_read_workqueue' in dm_table.stdout" + - "'no_write_workqueue' in dm_table.stdout" + - "'same_cpu_crypt' in dm_table.stdout" + - "'submit_from_crypt_cpus' in dm_table.stdout" + + - name: Closed and Removed + luks_device: + name: "{{ cryptfile_device }}" + state: absent + become: true + + when: + - ansible_facts.kernel is version('5.9.0', '>=') + - cryptsetup_version is version('2.3.4', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/vars/Alpine.yml b/ansible_collections/community/crypto/tests/integration/targets/luks_device/vars/Alpine.yml new file mode 100644 index 000000000..c0d230abf --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/vars/Alpine.yml @@ -0,0 +1,10 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +cryptsetup_package: cryptsetup + +luks_extra_packages: + - device-mapper + - wipefs diff --git a/ansible_collections/community/crypto/tests/integration/targets/luks_device/vars/default.yml b/ansible_collections/community/crypto/tests/integration/targets/luks_device/vars/default.yml new file mode 100644 index 000000000..72ed39e75 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/luks_device/vars/default.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +cryptsetup_package: cryptsetup + +luks_extra_packages: [] diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/aliases new file mode 100644 index 000000000..326a499c3 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/meta/main.yml new file mode 100644 index 000000000..30ac4eab4 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_ssh_keygen + - setup_ssh_agent + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tasks/main.yml new file mode 100644 index 000000000..94782c95c --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tasks/main.yml @@ -0,0 +1,47 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Declare global variables + set_fact: + signing_key: '{{ remote_tmp_dir }}/id_key' + public_key: '{{ remote_tmp_dir }}/id_key.pub' + certificate_path: '{{ remote_tmp_dir }}/id_cert' + +- name: Generate keypair + openssh_keypair: + path: "{{ signing_key }}" + type: rsa + size: 1024 + +- block: + - name: Import idempotency tests + import_tasks: ../tests/idempotency.yml + + - name: Import key_idempotency tests + import_tasks: ../tests/key_idempotency.yml + + - name: Import options tests + import_tasks: ../tests/options_idempotency.yml + + - name: Import regenerate tests + import_tasks: ../tests/regenerate.yml + + - name: Import remove tests + import_tasks: ../tests/remove.yml + when: not (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] == "6") + +- name: Import ssh-agent tests + import_tasks: ../tests/ssh-agent.yml + when: openssh_version is version("7.6",">=") + +- name: Remove keypair + openssh_keypair: + path: "{{ signing_key }}" + state: absent diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/idempotency.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/idempotency.yml new file mode 100644 index 000000000..c83596997 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/idempotency.yml @@ -0,0 +1,289 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- set_fact: + test_cases: + - test_name: Generate cert - force option (check_mode) + force: true + type: user + valid_from: always + valid_to: forever + check_mode: true + changed: true + - test_name: Generate cert - force option + force: true + type: user + valid_from: always + valid_to: forever + check_mode: true + changed: true + - test_name: Generate cert - force option (idempotent) + force: true + type: user + valid_from: always + valid_to: forever + check_mode: true + changed: true + - test_name: Generate cert - force option (idemopotent, check mode) + force: true + type: user + valid_from: always + valid_to: forever + check_mode: true + changed: true + - test_name: Generate always valid cert (check mode) + type: user + valid_from: always + valid_to: forever + check_mode: true + changed: true + - test_name: Generate always valid cert + type: user + valid_from: always + valid_to: forever + changed: true + - test_name: Generate always valid cert (idempotent) + type: user + valid_from: always + valid_to: forever + changed: false + - test_name: Generate always valid cert (idempotent, check mode) + type: user + valid_from: always + valid_to: forever + check_mode: true + changed: false + - test_name: Generate restricted validity cert with valid_at (check mode) + type: host + valid_from: +0s + valid_to: +32w + valid_at: +2w + check_mode: true + changed: true + - test_name: Generate restricted validity cert with valid_at + type: host + valid_from: +0s + valid_to: +32w + valid_at: +2w + changed: true + # Relative date time is based on current time so re-generation will occur in this case + - test_name: Generate restricted validity cert with valid_at (idempotent) + type: host + valid_from: +0s + valid_to: +32w + valid_at: +2w + changed: true + # Relative date time is based on current time so re-generation will occur in this case + - test_name: Generate restricted validity cert with valid_at (idempotent, check mode) + type: host + valid_from: +0s + valid_to: +32w + valid_at: +2w + check_mode: true + changed: true + - test_name: Generate always valid cert only for example.com and examplehost (check mode) + type: host + valid_from: always + valid_to: forever + principals: &principals + - example.com + - examplehost + check_mode: true + changed: true + - test_name: Generate always valid cert only for example.com and examplehost + type: host + valid_from: always + valid_to: forever + principals: *principals + changed: true + - test_name: Generate always valid cert only for example.com and examplehost (idempotent) + type: host + valid_from: always + valid_to: forever + principals: *principals + changed: false + - test_name: Generate always valid cert only for example.com and examplehost (idempotent, check mode) + type: host + valid_from: always + valid_to: forever + principals: *principals + check_mode: true + changed: false + - test_name: Generate always valid cert only for example.com and examplehost (idempotent, switch) + type: host + valid_from: always + valid_to: forever + principals: + - examplehost + - example.com + changed: false + - test_name: Generate OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019 (check mode) + type: host + valid_from: "2001-01-21" + valid_to: "2019-01-21" + check_mode: true + changed: true + - test_name: Generate OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019 + type: host + valid_from: "2001-01-21" + valid_to: "2019-01-21" + changed: true + - test_name: Generate OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019 (idempotent) + type: host + valid_from: "2001-01-21" + valid_to: "2019-01-21" + changed: false + - test_name: Generate OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019 (idempotent, check mode) + type: host + valid_from: "2001-01-21" + valid_to: "2019-01-21" + check_mode: true + changed: false + - test_name: Generate an OpenSSH user Certificate with clear and force-command option (check mode) + type: user + options: &options + - "clear" + - "force-command=/tmp/bla/foo" + valid_from: "2001-01-21" + valid_to: "2019-01-21" + check_mode: true + changed: true + - test_name: Generate an OpenSSH user Certificate with clear and force-command option + type: user + options: *options + valid_from: "2001-01-21" + valid_to: "2019-01-21" + changed: true + - test_name: Generate an OpenSSH user Certificate with clear and force-command option (idempotent) + type: user + options: *options + valid_from: "2001-01-21" + valid_to: "2019-01-21" + changed: false + - test_name: Generate an OpenSSH user Certificate with clear and force-command option (idempotent, check mode) + type: user + options: *options + valid_from: "2001-01-21" + valid_to: "2019-01-21" + check_mode: true + changed: false + - test_name: Generate an OpenSSH user Certificate with clear and force-command option (idempotent, switch) + type: user + options: + - "force-command=/tmp/bla/foo" + - "clear" + valid_from: "2001-01-21" + valid_to: "2019-01-21" + changed: false + - test_name: Generate an OpenSSH user Certificate with no options (idempotent) + type: user + valid_from: "2001-01-21" + valid_to: "2019-01-21" + changed: false + - test_name: Generate an OpenSSH user Certificate with no options - full idempotency (idempotent) + type: user + valid_from: "2001-01-21" + valid_to: "2019-01-21" + regenerate: full_idempotence + changed: true + - test_name: Generate cert without serial + type: user + valid_from: always + valid_to: forever + changed: true + - test_name: Generate cert without serial (idempotent) + type: user + valid_from: always + valid_to: forever + changed: false + - test_name: Generate cert with serial 42 + type: user + valid_from: always + valid_to: forever + serial_number: 42 + changed: true + - test_name: Generate cert with serial 42 (idempotent) + type: user + valid_from: always + valid_to: forever + serial_number: 42 + changed: false + - test_name: Generate cert with changed serial number + type: user + valid_from: always + valid_to: forever + serial_number: 1337 + changed: true + - test_name: Generate cert with removed serial number + type: user + valid_from: always + valid_to: forever + serial_number: 0 + changed: true + - test_name: Generate a new cert with serial number + type: user + valid_from: always + valid_to: forever + serial_number: 42 + changed: true + - test_name: Generate cert again, omitting the parameter serial_number (idempotent) + type: user + valid_from: always + valid_to: forever + changed: false + - test_name: Generate cert with identifier + type: user + identifier: foo + valid_from: always + valid_to: forever + changed: false + - test_name: Generate cert with identifier - full idempotency + type: user + identifier: foo + valid_from: always + valid_to: forever + regenerate: full_idempotence + changed: true + +- name: Execute idempotency tests + openssh_cert: + force: "{{ test_case.force | default(omit) }}" + identifier: "{{ test_case.identifier | default(omit) }}" + options: "{{ test_case.options | default(omit) }}" + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + principals: "{{ test_case.principals | default(omit) }}" + serial_number: "{{ test_case.serial_number | default(omit) }}" + signing_key: "{{ signing_key }}" + state: "{{ test_case.state | default(omit) }}" + type: "{{ test_case.type | default(omit) }}" + valid_at: "{{ test_case.valid_at | default(omit) }}" + valid_from: "{{ test_case.valid_from | default(omit) }}" + valid_to: "{{ test_case.valid_to | default(omit) }}" + regenerate: "{{ test_case.regenerate | default(omit) }}" + check_mode: "{{ test_case.check_mode | default(omit) }}" + register: idempotency_test_output + loop: "{{ test_cases }}" + loop_control: + loop_var: test_case + +- name: Assert task statuses + assert: + that: + - result.changed == test_cases[index].changed + loop: "{{ idempotency_test_output.results }}" + loop_control: + index_var: index + loop_var: result + +- name: Remove certificate + openssh_cert: + path: "{{ certificate_path }}" + state: absent
\ No newline at end of file diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/key_idempotency.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/key_idempotency.yml new file mode 100644 index 000000000..d66886a0d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/key_idempotency.yml @@ -0,0 +1,165 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- set_fact: + new_signing_key: "{{ remote_tmp_dir }}/new_key" + new_public_key: "{{ remote_tmp_dir }}/new_key.pub" + +- name: Generate new test key + openssh_keypair: + path: "{{ new_signing_key }}" + +- name: Generate cert with original keys + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + +- block: + - name: Generate cert with updated signature algorithm + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + signature_algorithm: rsa-sha2-256 + valid_from: always + valid_to: forever + register: updated_signature_algorithm + + - name: Assert signature algorithm update causes change + assert: + that: + - updated_signature_algorithm is changed + + - name: Generate cert with updated signature algorithm (idempotent) + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + signature_algorithm: rsa-sha2-256 + valid_from: always + valid_to: forever + register: updated_signature_algorithm_idempotent + + - name: Assert signature algorithm update is idempotent + assert: + that: + - updated_signature_algorithm_idempotent is not changed + + - block: + - name: Generate cert with original signature algorithm + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + signature_algorithm: ssh-rsa + valid_from: always + valid_to: forever + register: second_signature_algorithm + + - name: Assert second signature algorithm update causes change + assert: + that: + - second_signature_algorithm is changed + # RHEL9 disables SHA-1 algorithms by default making this test fail with a 'libcrypt' error. Other systems which + # impose a similar restriction may also need to skip this block in the future. + when: not (ansible_facts['distribution'] == "RedHat" and (ansible_facts['distribution_major_version'] | int) >= 9) + + - name: Omit signature algorithm + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + register: omitted_signature_algorithm + + - name: Assert omitted_signature_algorithm does not cause change + assert: + that: + - omitted_signature_algorithm is not changed + + - name: Revert to original certificate + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + regenerate: always + when: openssh_version is version("7.3", ">=") + +- name: Generate cert with new signing key + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ new_signing_key }}" + valid_from: always + valid_to: forever + register: new_signing_key_output + +- name: Generate cert with new public key + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ new_public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + register: new_public_key_output + +- name: Generate cert with new signing key - full idempotency + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ new_signing_key }}" + valid_from: always + valid_to: forever + regenerate: full_idempotence + register: new_signing_key_full_idempotency_output + +- name: Generate cert with new pubic key - full idempotency + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ new_public_key }}" + signing_key: "{{ new_signing_key }}" + valid_from: always + valid_to: forever + regenerate: full_idempotence + register: new_public_key_full_idempotency_output + +- name: Assert changes to public key or signing key results in no change unless idempotency=full + assert: + that: + - new_signing_key_output is not changed + - new_public_key_output is not changed + - new_signing_key_full_idempotency_output is changed + - new_public_key_full_idempotency_output is changed + +- name: Remove certificate + openssh_cert: + path: "{{ certificate_path }}" + state: absent + +- name: Remove new keypair + openssh_keypair: + path: "{{ new_signing_key }}" + state: absent diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/options_idempotency.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/options_idempotency.yml new file mode 100644 index 000000000..cc7a1d4be --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/options_idempotency.yml @@ -0,0 +1,184 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Generate cert with no options + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + options: + - clear + regenerate: full_idempotence + register: no_options + +- name: Generate cert with no options with explicit directives + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + options: + - no-user-rc + - no-x11-forwarding + - no-agent-forwarding + - no-port-forwarding + - no-pty + regenerate: full_idempotence + register: no_options_explicit_directives + +- name: Generate cert with explicit extension + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + options: + - clear + - permit-pty + regenerate: full_idempotence + register: explicit_extension_before + +- name: Generate cert with explicit extension (idempotency) + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + options: + - clear + - permit-pty + regenerate: full_idempotence + register: explicit_extension_after + +- name: Generate cert with explicit extension and corresponding directive + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + options: + - no-pty + - permit-pty + regenerate: full_idempotence + register: explicit_extension_and_directive + +- name: Generate cert with default options + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + regenerate: full_idempotence + register: default_options + +- name: Generate cert with relative timestamp + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: +0s + valid_to: +32w + valid_at: +2w + regenerate: full_idempotence + register: relative_timestamp + +- name: Generate cert with ignore_timestamp true + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: +0s + valid_to: +32w + valid_at: +2w + ignore_timestamps: true + regenerate: full_idempotence + register: relative_timestamp_true + +- name: Generate cert with ignore_timestamp false + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: +0s + valid_to: +32w + valid_at: +2w + ignore_timestamps: false + regenerate: full_idempotence + register: relative_timestamp_false + +- name: Generate cert with ignore_timestamp true + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: +0s + valid_to: +32w + valid_at: +50w + ignore_timestamps: true + regenerate: full_idempotence + register: relative_timestamp_invalid_at + +- name: Generate host cert full_idempotence + openssh_cert: + type: host + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + regenerate: full_idempotence + +- name: Generate host cert full_idempotence again + openssh_cert: + type: host + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + regenerate: full_idempotence + register: host_cert_full_idempotence + +- name: Assert options results + assert: + that: + - no_options is changed + - no_options_explicit_directives is not changed + - explicit_extension_before is changed + - explicit_extension_after is not changed + - explicit_extension_and_directive is changed + - default_options is not changed + - relative_timestamp is changed + - relative_timestamp_true is not changed + - relative_timestamp_false is changed + - relative_timestamp_invalid_at is changed + - host_cert_full_idempotence is not changed + +- name: Remove certificate + openssh_cert: + path: "{{ certificate_path }}" + state: absent diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/regenerate.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/regenerate.yml new file mode 100644 index 000000000..39fe860d2 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/regenerate.yml @@ -0,0 +1,140 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- set_fact: + test_cases: + - test_name: Generate certificate + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: "{{ certificate_path }}" + valid_from: always + valid_to: forever + regenerate: never + changed: true + - test_name: Regenerate never - same options + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: "{{ certificate_path }}" + valid_from: always + valid_to: forever + regenerate: never + changed: false + - test_name: Regenerate never - different options + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: "{{ certificate_path }}" + valid_from: always + valid_to: forever + options: + - clear + regenerate: never + changed: false + - test_name: Regenerate never with force + force: true + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: "{{ certificate_path }}" + valid_from: always + valid_to: forever + regenerate: never + changed: true + - test_name: Remove certificate + path: "{{ certificate_path }}" + state: absent + changed: true + - test_name: Regenerate fail - new certificate + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: "{{ certificate_path }}" + valid_from: always + valid_to: forever + regenerate: fail + changed: true + - test_name: Regenerate fail - same options + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: "{{ certificate_path }}" + valid_from: always + valid_to: forever + regenerate: fail + changed: false + - test_name: Regenerate fail - different options + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: "{{ certificate_path }}" + valid_from: always + valid_to: forever + options: + - clear + regenerate: fail + changed: false + ignore_errors: true + - test_name: Regenerate fail with force + force: true + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: "{{ certificate_path }}" + valid_from: always + valid_to: forever + regenerate: fail + changed: true + - test_name: Regenerate always + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: "{{ certificate_path }}" + valid_from: always + valid_to: forever + regenerate: always + changed: true + +- name: Execute regenerate tests + openssh_cert: + force: "{{ test_case.force | default(omit) }}" + options: "{{ test_case.options | default(omit) }}" + path: "{{ test_case.path | default(omit) }}" + public_key: "{{ test_case.public_key | default(omit) }}" + principals: "{{ test_case.principals | default(omit) }}" + regenerate: "{{ test_case.regenerate | default(omit) }}" + serial_number: "{{ test_case.serial_number | default(omit) }}" + signing_key: "{{ test_case.signing_key | default(omit) }}" + state: "{{ test_case.state | default(omit) }}" + type: "{{ test_case.type | default(omit) }}" + valid_at: "{{ test_case.valid_at | default(omit) }}" + valid_from: "{{ test_case.valid_from | default(omit) }}" + valid_to: "{{ test_case.valid_to | default(omit) }}" + check_mode: "{{ test_case.check_mode | default(omit) }}" + ignore_errors: "{{ test_case.ignore_errors | default(omit) }}" + register: regenerate_tests_output + loop: "{{ test_cases }}" + loop_control: + loop_var: test_case + +- name: Assert task statuses + assert: + that: + - result.changed == test_cases[index].changed + loop: "{{ regenerate_tests_output.results }}" + loop_control: + index_var: index + loop_var: result + +- name: Remove certificate + openssh_cert: + path: "{{ certificate_path }}" + state: absent diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/remove.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/remove.yml new file mode 100644 index 000000000..fcae35134 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/remove.yml @@ -0,0 +1,66 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- set_fact: + test_cases: + - test_name: Generate certificate + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: "{{ certificate_path }}" + valid_from: always + valid_to: forever + changed: true + - test_name: Remove certificate (check mode) + state: absent + path: "{{ certificate_path }}" + check_mode: true + changed: true + - test_name: Remove certificate + state: absent + path: "{{ certificate_path }}" + changed: true + - test_name: Remove certificate (idempotent) + state: absent + path: "{{ certificate_path }}" + changed: false + - test_name: Remove certificate (idempotent, check mode) + state: absent + path: "{{ certificate_path }}" + check_mode: true + changed: false + +- name: Execute remove tests + openssh_cert: + options: "{{ test_case.options | default(omit) }}" + path: "{{ test_case.path | default(omit) }}" + public_key: "{{ test_case.public_key | default(omit) }}" + principals: "{{ test_case.principals | default(omit) }}" + serial_number: "{{ test_case.serial_number | default(omit) }}" + signing_key: "{{ test_case.signing_key | default(omit) }}" + state: "{{ test_case.state | default(omit) }}" + type: "{{ test_case.type | default(omit) }}" + valid_at: "{{ test_case.valid_at | default(omit) }}" + valid_from: "{{ test_case.valid_from | default(omit) }}" + valid_to: "{{ test_case.valid_to | default(omit) }}" + check_mode: "{{ test_case.check_mode | default(omit) }}" + register: remove_test_output + loop: "{{ test_cases }}" + loop_control: + loop_var: test_case + +- name: Assert task statuses + assert: + that: + - result.changed == test_cases[index].changed + loop: "{{ remove_test_output.results }}" + loop_control: + index_var: index + loop_var: result diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/ssh-agent.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/ssh-agent.yml new file mode 100644 index 000000000..1f0c82294 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_cert/tests/ssh-agent.yml @@ -0,0 +1,88 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: SSH-agent test block + environment: + SSH_AUTH_SOCK: "{{ openssh_agent_sock }}" + block: + - name: Generate always valid cert using agent without key in agent (should fail) + openssh_cert: + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: '{{ remote_tmp_dir }}/id_cert_with_agent' + use_agent: true + valid_from: always + valid_to: forever + register: rc_no_key_in_agent + ignore_errors: true + + - name: Make sure cert creation with agent fails if key not in agent + assert: + that: + - rc_no_key_in_agent is failed + - "'agent contains no identities' in rc_no_key_in_agent.msg or 'not found in agent' in rc_no_key_in_agent.msg" + + - name: Add key to agent + command: 'ssh-add {{ signing_key }}' + + - name: Generate always valid cert with agent (check mode) + openssh_cert: + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: '{{ remote_tmp_dir }}/id_cert_with_agent' + use_agent: true + valid_from: always + valid_to: forever + check_mode: true + + - name: Generate always valid cert with agent + openssh_cert: + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: '{{ remote_tmp_dir }}/id_cert_with_agent' + use_agent: true + valid_from: always + valid_to: forever + + - name: Generate always valid cert with agent (idempotent) + openssh_cert: + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: '{{ remote_tmp_dir }}/id_cert_with_agent' + use_agent: true + valid_from: always + valid_to: forever + register: rc_cert_with_agent_idempotent + + - name: Check agent idempotency + assert: + that: + - rc_cert_with_agent_idempotent is not changed + msg: OpenSSH certificate generation without serial number is idempotent. + + - name: Generate always valid cert with agent (idempotent, check mode) + openssh_cert: + type: user + signing_key: "{{ signing_key }}" + public_key: "{{ public_key }}" + path: '{{ remote_tmp_dir }}/id_cert_with_agent' + use_agent: true + valid_from: always + valid_to: forever + check_mode: true + + - name: Remove certificate + openssh_cert: + state: absent + path: '{{ remote_tmp_dir }}/id_cert_with_agent' diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/aliases new file mode 100644 index 000000000..326a499c3 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/meta/main.yml new file mode 100644 index 000000000..649911a9c --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/meta/main.yml @@ -0,0 +1,10 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_ssh_keygen + - setup_openssl + - setup_bcrypt + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tasks/main.yml new file mode 100644 index 000000000..274008249 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tasks/main.yml @@ -0,0 +1,50 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Backend auto-detection test + openssh_keypair: + path: '{{ remote_tmp_dir }}/auto_backend_key' + state: "{{ item }}" + loop: ['present', 'absent'] + +- set_fact: + backends: ['opensshbin'] + +- set_fact: + backends: "{{ backends + ['cryptography'] }}" + when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=') + +- include_tasks: ../tests/core.yml + loop: "{{ backends }}" + loop_control: + loop_var: backend + +- include_tasks: ../tests/invalid.yml + loop: "{{ backends }}" + loop_control: + loop_var: backend + +- include_tasks: ../tests/options.yml + loop: "{{ backends }}" + loop_control: + loop_var: backend + +- include_tasks: ../tests/regenerate.yml + loop: "{{ backends }}" + loop_control: + loop_var: backend + +- include_tasks: ../tests/state.yml + loop: "{{ backends }}" + loop_control: + loop_var: backend + +- include_tasks: ../tests/cryptography_backend.yml + when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/core.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/core.yml new file mode 100644 index 000000000..a0182b485 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/core.yml @@ -0,0 +1,103 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: "({{ backend }}) Generate key (check mode)" + openssh_keypair: + path: "{{ remote_tmp_dir }}/core" + size: 1280 + backend: "{{ backend }}" + register: check_core_output + check_mode: true + +- name: "({{ backend }}) Generate key" + openssh_keypair: + path: "{{ remote_tmp_dir }}/core" + size: 1280 + backend: "{{ backend }}" + register: core_output + +- name: "({{ backend }}) Generate key (check mode idempotent)" + openssh_keypair: + path: "{{ remote_tmp_dir }}/core" + size: 1280 + backend: "{{ backend }}" + register: idempotency_check_core_output + check_mode: true + +- name: "({{ backend }}) Generate key (idempotent)" + openssh_keypair: + path: '{{ remote_tmp_dir }}/core' + size: 1280 + backend: "{{ backend }}" + register: idempotency_core_output + +- name: "({{ backend }}) Log key return values" + debug: + msg: "{{ core_output }}" + +- name: "({{ backend }}) Assert core behavior" + assert: + that: + - check_core_output is changed + - core_output is changed + - idempotency_check_core_output is not changed + - idempotency_check_core_output.public_key.startswith('ssh-rsa') + - idempotency_core_output is not changed + +- name: "({{ backend }}) Assert key returns fingerprint" + assert: + that: + - core_output['fingerprint'] is string + - core_output['fingerprint'].startswith('SHA256:') + # SHA256 was made the default hashing algorithm for fingerprints in OpenSSH 6.8 + when: not (backend == 'opensshbin' and openssh_version is version('6.8', '<')) + +- name: "({{ backend }}) Assert key returns public_key" + assert: + that: + - core_output['public_key'] is string + - core_output['public_key'].startswith('ssh-rsa ') + +- name: "({{ backend }}) Assert key returns size value" + assert: + that: + - core_output['size']|type_debug == 'int' + - core_output['size'] == 1280 + +- name: "({{ backend }}) Assert key returns key type" + assert: + that: + - core_output['type'] is string + - core_output['type'] == 'rsa' + +- name: "({{ backend }}) Retrieve key size from 'ssh-keygen'" + shell: "ssh-keygen -lf {{ remote_tmp_dir }}/core | grep -o -E '^[0-9]+'" + register: core_size_ssh_keygen + +- name: "({{ backend }}) Assert key size matches 'ssh-keygen' output" + assert: + that: + - core_size_ssh_keygen.stdout == '1280' + +- name: "({{ backend }}) Read core.pub" + slurp: + src: '{{ remote_tmp_dir }}/core.pub' + register: slurp + +- name: "({{ backend }}) Assert public key module return equal to the public key content" + assert: + that: + - "core_output.public_key == (slurp.content | b64decode).strip('\n ')" + +- name: "({{ backend }}) Remove key" + openssh_keypair: + path: '{{ remote_tmp_dir }}/core' + backend: "{{ backend }}" + state: absent diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/cryptography_backend.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/cryptography_backend.yml new file mode 100644 index 000000000..b72c0be68 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/cryptography_backend.yml @@ -0,0 +1,169 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Generate a password protected key + command: 'ssh-keygen -f {{ remote_tmp_dir }}/password_protected -N {{ passphrase }}' + +- name: Modify the password protected key with passphrase + openssh_keypair: + path: '{{ remote_tmp_dir }}/password_protected' + size: 1024 + passphrase: "{{ passphrase }}" + backend: cryptography + register: password_protected_output + +- name: Check password protected key idempotency + openssh_keypair: + path: '{{ remote_tmp_dir }}/password_protected' + size: 1024 + passphrase: "{{ passphrase }}" + backend: cryptography + register: password_protected_idempotency_output + +- name: Ensure that ssh-keygen can read keys generated with passphrase + command: 'ssh-keygen -yf {{ remote_tmp_dir }}/password_protected -P {{ passphrase }}' + register: password_protected_ssh_keygen_output + +- name: Check that password protected key with passphrase was regenerated + assert: + that: + - password_protected_output is changed + - password_protected_idempotency_output is not changed + - password_protected_ssh_keygen_output is success + +- name: Remove password protected key + openssh_keypair: + path: '{{ remote_tmp_dir }}/password_protected' + backend: cryptography + state: absent + +- name: Generate an unprotected key + openssh_keypair: + path: '{{ remote_tmp_dir }}/unprotected' + backend: cryptography + +- name: Modify unprotected key with passphrase + openssh_keypair: + path: '{{ remote_tmp_dir }}/unprotected' + size: 1280 + passphrase: "{{ passphrase }}" + backend: cryptography + ignore_errors: true + register: unprotected_modification_output + +- name: Modify unprotected key with passphrase (force) + openssh_keypair: + path: '{{ remote_tmp_dir }}/unprotected' + size: 1280 + passphrase: "{{ passphrase }}" + force: true + backend: cryptography + register: force_unprotected_modification_output + +- name: Check that unprotected key was modified + assert: + that: + - unprotected_modification_output is failed + - force_unprotected_modification_output is changed + +- name: Remove unprotected key + openssh_keypair: + path: '{{ remote_tmp_dir }}/unprotected' + backend: cryptography + state: absent + +- name: Generate PEM encoded key with passphrase + command: 'ssh-keygen -b 1280 -f {{ remote_tmp_dir }}/pem_encoded -N {{ passphrase }} -m PEM' + +- name: Try to verify a PEM encoded key + openssh_keypair: + path: '{{ remote_tmp_dir }}/pem_encoded' + passphrase: "{{ passphrase }}" + backend: cryptography + size: 1280 + register: pem_encoded_output + +- name: Check that PEM encoded file is read without errors + assert: + that: + - pem_encoded_output is not changed + +- name: Remove PEM encoded key + openssh_keypair: + path: '{{ remote_tmp_dir }}/pem_encoded' + backend: cryptography + state: absent + +- name: Generate a private key with specified format + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + private_key_format: pkcs1 + backend: cryptography + +- name: Generate a private key with specified format (Idempotent) + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + private_key_format: pkcs1 + backend: cryptography + register: private_key_format_idempotent + +- name: Check that private key with specified format is idempotent + assert: + that: + - private_key_format_idempotent is not changed + +- name: Change to PKCS8 format + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + private_key_format: pkcs8 + backend: cryptography + register: private_key_format_pkcs8 + +- name: Check that format change causes regeneration + assert: + that: + - private_key_format_pkcs8 is changed + +- name: Change to PKCS8 format (Idempotent) + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + private_key_format: pkcs8 + backend: cryptography + register: private_key_format_pkcs8_idempotent + +- name: Check that private key with PKCS8 format is idempotent + assert: + that: + - private_key_format_pkcs8_idempotent is not changed + +- name: Change to SSH format + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + private_key_format: ssh + backend: cryptography + register: private_key_format_ssh + +- name: Check that format change causes regeneration + assert: + that: + - private_key_format_ssh is changed + +- name: Change to SSH format (Idempotent) + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + private_key_format: ssh + backend: cryptography + register: private_key_format_ssh_idempotent + +- name: Check that private key with SSH format is idempotent + assert: + that: + - private_key_format_ssh_idempotent is not changed + +- name: Remove private key with specified format + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + backend: cryptography + state: absent diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/invalid.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/invalid.yml new file mode 100644 index 000000000..35b749f77 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/invalid.yml @@ -0,0 +1,135 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: "({{ backend }}) Generate key - broken" + copy: + dest: '{{ item }}' + content: '' + mode: '0700' + loop: + - "{{ remote_tmp_dir }}/broken" + - "{{ remote_tmp_dir }}/broken.pub" + +- name: "({{ backend }}) Regenerate key - broken" + openssh_keypair: + path: "{{ remote_tmp_dir }}/broken" + backend: "{{ backend }}" + register: broken_output + ignore_errors: true + +- name: "({{ backend }}) Assert broken key causes failure - broken" + assert: + that: + - broken_output is failed + - "'Unable to read the key. The key is protected with a passphrase or broken.' in broken_output.msg" + +- name: "({{ backend }}) Regenerate key with force - broken" + openssh_keypair: + path: "{{ remote_tmp_dir }}/broken" + backend: "{{ backend }}" + force: true + register: force_broken_output + +- name: "({{ backend }}) Assert broken key regenerated when 'force=true' - broken" + assert: + that: + - force_broken_output is changed + +- name: "({{ backend }}) Remove key - broken" + openssh_keypair: + path: "{{ remote_tmp_dir }}/broken" + backend: "{{ backend }}" + state: absent + +- name: "({{ backend }}) Generate key - write-only" + openssh_keypair: + path: "{{ remote_tmp_dir }}/write-only" + mode: "0200" + backend: "{{ backend }}" + +- name: "({{ backend }}) Check private key status - write-only" + stat: + path: '{{ remote_tmp_dir }}/write-only' + register: write_only_private_key + +- name: "({{ backend }}) Check public key status - write-only" + stat: + path: '{{ remote_tmp_dir }}/write-only.pub' + register: write_only_public_key + +- name: "({{ backend }}) Assert that private and public keys match permissions - write-only" + assert: + that: + - write_only_private_key.stat.mode == '0200' + - write_only_public_key.stat.mode == '0200' + +- name: "({{ backend }}) Regenerate key with force - write-only" + openssh_keypair: + path: "{{ remote_tmp_dir }}/write-only" + backend: "{{ backend }}" + force: true + register: write_only_output + +- name: "({{ backend }}) Check private key status after regeneration - write-only" + stat: + path: '{{ remote_tmp_dir }}/write-only' + register: write_only_private_key_after + +- name: "({{ backend }}) Assert key is regenerated - write-only" + assert: + that: + - write_only_output is changed + +- name: "({{ backend }}) Assert key permissions are preserved with 'opensshbin'" + assert: + that: + - write_only_private_key_after.stat.mode == '0200' + +- name: "({{ backend }}) Remove key - write-only" + openssh_keypair: + path: "{{ remote_tmp_dir }}/write-only" + backend: "{{ backend }}" + state: absent + +- name: "({{ backend }}) Generate key with ssh-keygen - password_protected" + command: "ssh-keygen -f {{ remote_tmp_dir }}/password_protected -N {{ passphrase }}" + +- name: "({{ backend }}) Modify key - password_protected" + openssh_keypair: + path: "{{ remote_tmp_dir }}/password_protected" + size: 1280 + backend: "{{ backend }}" + register: password_protected_output + ignore_errors: true + +- name: "({{ backend }}) Assert key cannot be read - password_protected" + assert: + that: + - password_protected_output is failed + - "'Unable to read the key. The key is protected with a passphrase or broken.' in password_protected_output.msg" + +- name: "({{ backend }}) Modify key with 'force=true' - password_protected" + openssh_keypair: + path: "{{ remote_tmp_dir }}/password_protected" + size: 1280 + backend: "{{ backend }}" + force: true + register: force_password_protected_output + +- name: "({{ backend }}) Assert key regenerated with 'force=true' - password_protected" + assert: + that: + - force_password_protected_output is changed + +- name: "({{ backend }}) Remove key - password_protected" + openssh_keypair: + path: "{{ remote_tmp_dir }}/password_protected" + backend: "{{ backend }}" + state: absent diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/options.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/options.yml new file mode 100644 index 000000000..fdabd7614 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/options.yml @@ -0,0 +1,121 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- set_fact: + key_types: + - rsa + - dsa + - ecdsa + +- name: "({{ backend }}) Generate keys with default size - size" + openssh_keypair: + path: "{{ remote_tmp_dir }}/default_size_{{ item }}" + type: "{{ item }}" + backend: "{{ backend }}" + loop: "{{ key_types }}" + +- name: "({{ backend }}) Retrieve key size from 'ssh-keygen' - size" + shell: "ssh-keygen -lf {{ remote_tmp_dir }}/default_size_{{ item }} | grep -o -E '^[0-9]+'" + loop: "{{ key_types }}" + register: key_size_output + +- name: "({{ backend }}) Assert key sizes match default size - size" + assert: + that: + - key_size_output.results[0].stdout == '4096' + - key_size_output.results[1].stdout == '1024' + - key_size_output.results[2].stdout == '256' + +- name: "({{ backend }}) Remove keys - size" + openssh_keypair: + path: "{{ remote_tmp_dir }}/default_size_{{ item }}" + state: absent + loop: "{{ key_types }}" + +- block: + - name: "({{ backend }}) Generate ed25519 key with default size - size" + openssh_keypair: + path: "{{ remote_tmp_dir }}/default_size_ed25519" + type: ed25519 + backend: "{{ backend }}" + + - name: "({{ backend }}) Retrieve ed25519 key size from 'ssh-keygen' - size" + shell: "ssh-keygen -lf {{ remote_tmp_dir }}/default_size_ed25519 | grep -o -E '^[0-9]+'" + register: ed25519_key_size_output + + - name: "({{ backend }}) Assert ed25519 key size matches default size - size" + assert: + that: + - ed25519_key_size_output.stdout == '256' + + - name: "({{ backend }}) Remove ed25519 key - size" + openssh_keypair: + path: "{{ remote_tmp_dir }}/default_size_ed25519" + state: absent + # Support for ed25519 keys was added in OpenSSH 6.5 + when: not (backend == 'opensshbin' and openssh_version is version('6.5', '<')) + +- name: "({{ backend }}) Generate key - force" + openssh_keypair: + path: "{{ remote_tmp_dir }}/force" + type: rsa + backend: "{{ backend }}" + +- name: "({{ backend }}) Regenerate key - force" + openssh_keypair: + path: "{{ remote_tmp_dir }}/force" + type: rsa + force: true + backend: "{{ backend }}" + register: force_output + +- name: "({{ backend }}) Assert key regenerated - force" + assert: + that: + - force_output is changed + +- name: "({{ backend }}) Remove key - force" + openssh_keypair: + path: "{{ remote_tmp_dir }}/force" + state: absent + backend: "{{ backend }}" + +- name: "({{ backend }}) Generate key - comment" + openssh_keypair: + path: "{{ remote_tmp_dir }}/comment" + comment: "test@comment" + backend: "{{ backend }}" + register: comment_output + +- name: "({{ backend }}) Modify comment - comment" + openssh_keypair: + path: "{{ remote_tmp_dir }}/comment" + comment: "test_modified@comment" + backend: "{{ backend }}" + register: modified_comment_output + +- name: "({{ backend }}) Assert comment preserved public key - comment" + assert: + that: + - comment_output.public_key == modified_comment_output.public_key + - comment_output.comment == 'test@comment' + +- name: "({{ backend }}) Assert comment changed - comment" + assert: + that: + - modified_comment_output.comment == 'test_modified@comment' + # Support for updating comments for key types other than rsa1 was added in OpenSSH 7.2 + when: not (backend == 'opensshbin' and openssh_version is version('7.2', '<')) + +- name: "({{ backend }}) Remove key - comment" + openssh_keypair: + path: "{{ remote_tmp_dir }}/comment" + state: absent + backend: "{{ backend }}" diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/regenerate.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/regenerate.yml new file mode 100644 index 000000000..d10096044 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/regenerate.yml @@ -0,0 +1,350 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Ensures no conflicts from previous test runs +- name: "({{ backend }}) Find old test artifacts" + ansible.builtin.find: + paths: "{{ remote_tmp_dir }}" + patterns: + - "regenerate*" + register: old_test_artifacts + +- name: "({{ backend }}) Cleanup Output Directory" + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + loop: "{{ old_test_artifacts.files }}" + +- name: "({{ backend }}) Regenerate - setup simple keys" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1024 + backend: "{{ backend }}" + regenerate: "{{ item }}" + loop: "{{ regenerate_values }}" +- name: "({{ backend }}) Regenerate - setup password protected keys" + command: 'ssh-keygen -f {{ remote_tmp_dir }}/regenerate-b-{{ item }} -N {{ passphrase }}' + loop: "{{ regenerate_values }}" + +- name: "({{ backend }}) Regenerate - setup broken keys" + copy: + dest: '{{ remote_tmp_dir }}/regenerate-c-{{ item.0 }}{{ item.1 }}' + content: 'broken key' + mode: '0700' + with_nested: + - "{{ regenerate_values }}" + - [ '', '.pub' ] + +- name: "({{ backend }}) Regenerate - setup password protected keys for passphrse test" + command: 'ssh-keygen -f {{ remote_tmp_dir }}/regenerate-d-{{ item }} -N {{ passphrase }}' + loop: "{{ regenerate_values }}" + +- name: "({{ backend }}) Regenerate - modify broken keys (check mode)" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-c-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + backend: "{{ backend }}" + check_mode: true + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ backend }}) Regenerate - modify broken keys" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-c-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + backend: "{{ backend }}" + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ backend }}) Regenerate - modify password protected keys (check mode)" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-b-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + backend: "{{ backend }}" + check_mode: true + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ backend }}) Regenerate - modify password protected keys with passphrase (check mode)" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-b-{{ item }}' + type: rsa + size: 1024 + passphrase: "{{ passphrase }}" + regenerate: '{{ item }}' + backend: "{{ backend }}" + check_mode: true + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result + when: backend == 'cryptography' + +- assert: + that: + - result.results[0] is success + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + when: backend == 'cryptography' + +- name: "({{ backend }}) Regenerate - modify password protected keys" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-b-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + backend: "{{ backend }}" + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ backend }}) Regenerate - modify password protected keys with passphrase" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-d-{{ item }}' + type: rsa + size: 1024 + passphrase: "{{ passphrase }}" + regenerate: '{{ item }}' + backend: "{{ backend }}" + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result + when: backend == 'cryptography' + +- assert: + that: + - result.results[0] is success + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + when: backend == 'cryptography' + +- name: "({{ backend }}) Regenerate - not modify regular keys (check mode)" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + backend: "{{ backend }}" + check_mode: true + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result.results[0] is not changed + - result.results[1] is not changed + - result.results[2] is not changed + - result.results[3] is not changed + - result.results[4] is changed + +- name: "({{ backend }}) Regenerate - not modify regular keys" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1024 + regenerate: '{{ item }}' + backend: "{{ backend }}" + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result.results[0] is not changed + - result.results[1] is not changed + - result.results[2] is not changed + - result.results[3] is not changed + - result.results[4] is changed + +- name: "({{ backend }}) Regenerate - adjust key size (check mode)" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1048 + regenerate: '{{ item }}' + backend: "{{ backend }}" + check_mode: true + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ backend }}) Regenerate - adjust key size" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}' + type: rsa + size: 1048 + regenerate: '{{ item }}' + backend: "{{ backend }}" + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ backend }}) Regenerate - redistribute keys" + copy: + src: '{{ remote_tmp_dir }}/regenerate-a-always{{ item.1 }}' + dest: '{{ remote_tmp_dir }}/regenerate-a-{{ item.0 }}{{ item.1 }}' + remote_src: true + with_nested: + - "{{ regenerate_values }}" + - [ '', '.pub' ] + when: "item.0 != 'always'" + +- name: "({{ backend }}) Regenerate - adjust key type (check mode)" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}' + type: dsa + size: 1024 + regenerate: '{{ item }}' + backend: "{{ backend }}" + check_mode: true + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ backend }}) Regenerate - adjust key type" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}' + type: dsa + size: 1024 + regenerate: '{{ item }}' + backend: "{{ backend }}" + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ backend }}) Regenerate - redistribute keys" + copy: + src: '{{ remote_tmp_dir }}/regenerate-a-always{{ item.1 }}' + dest: '{{ remote_tmp_dir }}/regenerate-a-{{ item.0 }}{{ item.1 }}' + remote_src: true + with_nested: + - "{{ regenerate_values }}" + - [ '', '.pub' ] + when: "item.0 != 'always'" + +- name: "({{ backend }}) Regenerate - adjust comment (check mode)" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}' + type: dsa + size: 1024 + comment: test comment + regenerate: '{{ item }}' + backend: "{{ backend }}" + check_mode: true + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result is changed + +- name: "({{ backend }}) Regenerate - adjust comment" + openssh_keypair: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}' + type: dsa + size: 1024 + comment: test comment + regenerate: '{{ item }}' + backend: "{{ backend }}" + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result is changed + # for all values but 'always', the key should not be regenerated. + # verify this by comparing fingerprints: + - result.results[0].fingerprint == result.results[1].fingerprint + - result.results[0].fingerprint == result.results[2].fingerprint + - result.results[0].fingerprint == result.results[3].fingerprint + - result.results[0].fingerprint != result.results[4].fingerprint diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/state.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/state.yml new file mode 100644 index 000000000..70f129d4e --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/tests/state.yml @@ -0,0 +1,49 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: "({{ backend }}) Generate key" + openssh_keypair: + path: '{{ remote_tmp_dir }}/removed' + backend: "{{ backend }}" + state: present + +- name: "({{ backend }}) Generate key (idempotency)" + openssh_keypair: + path: '{{ remote_tmp_dir }}/removed' + backend: "{{ backend }}" + state: present + +- name: "({{ backend }}) Remove key" + openssh_keypair: + state: absent + path: '{{ remote_tmp_dir }}/removed' + backend: "{{ backend }}" + +- name: "({{ backend }}) Remove key (idempotency)" + openssh_keypair: + state: absent + path: '{{ remote_tmp_dir }}/removed' + backend: "{{ backend }}" + +- name: "({{ backend }}) Check private key status" + stat: + path: '{{ remote_tmp_dir }}/removed' + register: removed_private_key + +- name: "({{ backend }}) Check public key status" + stat: + path: '{{ remote_tmp_dir }}/removed.pub' + register: removed_public_key + +- name: "({{ backend }}) Assert key pair files are removed" + assert: + that: + - not removed_private_key.stat.exists + - not removed_public_key.stat.exists diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/vars/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/vars/main.yml new file mode 100644 index 000000000..141eff764 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssh_keypair/vars/main.yml @@ -0,0 +1,12 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +passphrase: password +regenerate_values: + - never + - fail + - partial_idempotence + - full_idempotence + - always diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/tasks/impl.yml new file mode 100644 index 000000000..7ac220e5a --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/tasks/impl.yml @@ -0,0 +1,1019 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "({{ select_crypto_backend }}) Generate privatekey" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey.pem' + size: '{{ default_rsa_key_size }}' + +- name: "({{ select_crypto_backend }}) Read privatekey" + slurp: + src: '{{ remote_tmp_dir }}/privatekey.pem' + register: privatekey + +- name: "({{ select_crypto_backend }}) Generate CSR (check mode)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + check_mode: true + register: generate_csr_check + +- name: "({{ select_crypto_backend }}) Generate CSR" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: generate_csr + +- name: "({{ select_crypto_backend }}) Generate CSR (idempotent)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_content: '{{ privatekey.content | b64decode }}' + subject_ordered: + - commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: generate_csr_idempotent + +- name: "({{ select_crypto_backend }}) Generate CSR (idempotent, check mode)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + check_mode: true + register: generate_csr_idempotent_check + +- name: "({{ select_crypto_backend }}) Generate CSR without SAN (check mode)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr-nosan.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + useCommonNameForSAN: false + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: generate_csr_nosan_check + +- name: "({{ select_crypto_backend }}) Generate CSR without SAN" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr-nosan.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + useCommonNameForSAN: false + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_csr_nosan + +- name: "({{ select_crypto_backend }}) Generate CSR without SAN (idempotent)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr-nosan.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + useCommonNameForSAN: false + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_csr_nosan_check_idempotent + +- name: "({{ select_crypto_backend }}) Generate CSR without SAN (idempotent, check mode)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr-nosan.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + useCommonNameForSAN: false + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: generate_csr_nosan_check_idempotent_check + +# keyUsage longname and shortname should be able to be used +# interchangeably. Hence the long name is specified here +# but the short name is used to test idempotency for ipsecuser +# and vice-versa for biometricInfo +- name: "({{ select_crypto_backend }}) Generate CSR with KU and XKU" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ku_xku.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + CN: www.ansible.com + keyUsage: + - digitalSignature + - keyAgreement + extendedKeyUsage: + - qcStatements + - DVCS + - IPSec User + - biometricInfo + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "({{ select_crypto_backend }}) Generate CSR with KU and XKU (test idempotency)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ku_xku.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: 'www.ansible.com' + keyUsage: + - Key Agreement + - digitalSignature + extendedKeyUsage: + - ipsecUser + - qcStatements + - DVCS + - Biometric Info + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_ku_xku + +- name: "({{ select_crypto_backend }}) Generate CSR with KU and XKU (test XKU change)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ku_xku.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: 'www.ansible.com' + keyUsage: + - digitalSignature + - keyAgreement + extendedKeyUsage: + - ipsecUser + - qcStatements + - Biometric Info + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_ku_xku_change + +- name: "({{ select_crypto_backend }}) Generate CSR with KU and XKU (test KU change)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ku_xku.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: 'www.ansible.com' + keyUsage: + - digitalSignature + extendedKeyUsage: + - ipsecUser + - qcStatements + - Biometric Info + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_ku_xku_change_2 + +- name: "({{ select_crypto_backend }}) Generate CSR with old API" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_oldapi.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "({{ select_crypto_backend }}) Generate CSR with invalid SAN (1/2)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csrinvsan.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject_alt_name: invalid-san.example.com + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_csr_invalid_san + ignore_errors: true + +- name: "({{ select_crypto_backend }}) Generate CSR with invalid SAN (2/2)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csrinvsan2.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject_alt_name: "DNS:system:kube-controller-manager" + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_csr_invalid_san_2 + ignore_errors: true + +- name: "({{ select_crypto_backend }}) Generate CSR with OCSP Must Staple" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ocsp.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject_alt_name: "DNS:www.ansible.com" + ocsp_must_staple: true + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "({{ select_crypto_backend }}) Generate CSR with OCSP Must Staple (test idempotency)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ocsp.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject_alt_name: "DNS:www.ansible.com" + ocsp_must_staple: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_ocsp_idempotency + +- name: "({{ select_crypto_backend }}) Generate ECC privatekey" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey2.pem' + type: ECC + curve: secp384r1 + +- name: "({{ select_crypto_backend }}) Generate CSR with ECC privatekey" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr2.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey2.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "({{ select_crypto_backend }}) Generate CSR with text common name" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr3.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey2.pem' + subject: + commonName: This is for Ansible + useCommonNameForSAN: false + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "({{ select_crypto_backend }}) Generate CSR with country name" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr4.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey2.pem' + country_name: de + select_crypto_backend: '{{ select_crypto_backend }}' + register: country_idempotent_1 + +- name: "({{ select_crypto_backend }}) Generate CSR with country name (idempotent)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr4.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey2.pem' + country_name: de + select_crypto_backend: '{{ select_crypto_backend }}' + register: country_idempotent_2 + +- name: "({{ select_crypto_backend }}) Generate CSR with country name (idempotent 2)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr4.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey2.pem' + subject: + C: de + select_crypto_backend: '{{ select_crypto_backend }}' + register: country_idempotent_3 + +- name: "({{ select_crypto_backend }}) Generate CSR with country name (bad country name)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr4.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey2.pem' + subject: + C: dex + select_crypto_backend: '{{ select_crypto_backend }}' + register: country_fail_4 + ignore_errors: true + +- name: "({{ select_crypto_backend }}) Generate privatekey with password" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + size: '{{ default_rsa_key_size }}' + +- name: "({{ select_crypto_backend }}) Read privatekey" + slurp: + src: '{{ remote_tmp_dir }}/privatekeypw.pem' + register: privatekeypw + +- name: "({{ select_crypto_backend }}) Generate CSR with privatekey passphrase" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_pw.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + privatekey_passphrase: hunter2 + select_crypto_backend: '{{ select_crypto_backend }}' + register: passphrase_1 + +- name: "({{ select_crypto_backend }}) Generate CSR with privatekey passphrase and private key content" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_pw.csr' + privatekey_content: '{{ privatekeypw.content | b64decode }}' + privatekey_passphrase: hunter2 + select_crypto_backend: '{{ select_crypto_backend }}' + register: passphrase_1_content + +- name: "({{ select_crypto_backend }}) Generate CSR (failed passphrase 1)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_pw1.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + privatekey_passphrase: hunter2 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: passphrase_error_1 + +- name: "({{ select_crypto_backend }}) Generate CSR (failed passphrase 2)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_pw2.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + privatekey_passphrase: wrong_password + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: passphrase_error_2 + +- name: "({{ select_crypto_backend }}) Generate CSR (failed passphrase 3)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_pw3.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: passphrase_error_3 + +- name: "({{ select_crypto_backend }}) Create broken CSR" + copy: + dest: "{{ remote_tmp_dir }}/csrbroken.csr" + content: "broken" +- name: "({{ select_crypto_backend }}) Regenerate broken CSR" + openssl_csr: + path: '{{ remote_tmp_dir }}/csrbroken.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey2.pem' + subject: + commonName: This is for Ansible + useCommonNameForSAN: false + select_crypto_backend: '{{ select_crypto_backend }}' + register: output_broken + +- name: "({{ select_crypto_backend }}) Generate CSR" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_backup.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_backup_1 +- name: "({{ select_crypto_backend }}) Generate CSR (idempotent)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_backup.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_backup_2 +- name: "({{ select_crypto_backend }}) Generate CSR (change)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_backup.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: ansible.com + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_backup_3 +- name: "({{ select_crypto_backend }}) Generate CSR (remove)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_backup.csr' + state: absent + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: csr_backup_4 +- name: "({{ select_crypto_backend }}) Generate CSR (remove, idempotent)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_backup.csr' + state: absent + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: csr_backup_5 + +- name: "({{ select_crypto_backend }}) Generate CSR with subject key identifier" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ski.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + subject_key_identifier: "00:11:22:33" + select_crypto_backend: '{{ select_crypto_backend }}' + register: subject_key_identifier_1 + +- name: "({{ select_crypto_backend }}) Generate CSR with subject key identifier (idempotency)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ski.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + subject_key_identifier: "00:11:22:33" + select_crypto_backend: '{{ select_crypto_backend }}' + register: subject_key_identifier_2 + +- name: "({{ select_crypto_backend }}) Generate CSR with subject key identifier (change)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ski.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + subject_key_identifier: "44:55:66:77:88" + select_crypto_backend: '{{ select_crypto_backend }}' + register: subject_key_identifier_3 + +- name: "({{ select_crypto_backend }}) Generate CSR with subject key identifier (auto-create)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ski.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + create_subject_key_identifier: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: subject_key_identifier_4 + +- name: "({{ select_crypto_backend }}) Generate CSR with subject key identifier (auto-create idempotency)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ski.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + create_subject_key_identifier: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: subject_key_identifier_5 + +- name: "({{ select_crypto_backend }}) Generate CSR with subject key identifier (remove)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ski.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + register: subject_key_identifier_6 + +- name: "({{ select_crypto_backend }}) Generate CSR with authority key identifier" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_aki.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_key_identifier: "00:11:22:33" + select_crypto_backend: '{{ select_crypto_backend }}' + register: authority_key_identifier_1 + +- name: "({{ select_crypto_backend }}) Generate CSR with authority key identifier (idempotency)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_aki.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_key_identifier: "00:11:22:33" + select_crypto_backend: '{{ select_crypto_backend }}' + register: authority_key_identifier_2 + +- name: "({{ select_crypto_backend }}) Generate CSR with authority key identifier (change)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_aki.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_key_identifier: "44:55:66:77:88" + select_crypto_backend: '{{ select_crypto_backend }}' + register: authority_key_identifier_3 + +- name: "({{ select_crypto_backend }}) Generate CSR with authority key identifier (remove)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_aki.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + register: authority_key_identifier_4 + +- name: "({{ select_crypto_backend }}) Generate CSR with authority cert issuer / serial number" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_acisn.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + register: authority_cert_issuer_sn_1 + +- name: "({{ select_crypto_backend }}) Generate CSR with authority cert issuer / serial number (idempotency)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_acisn.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + register: authority_cert_issuer_sn_2 + +- name: "({{ select_crypto_backend }}) Generate CSR with authority cert issuer / serial number (change issuer)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_acisn.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_cert_issuer: + - "IP:1.2.3.4" + - "DNS:ca.example.org" + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + register: authority_cert_issuer_sn_3 + +- name: "({{ select_crypto_backend }}) Generate CSR with authority cert issuer / serial number (change serial number)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_acisn.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_cert_issuer: + - "IP:1.2.3.4" + - "DNS:ca.example.org" + authority_cert_serial_number: 54321 + select_crypto_backend: '{{ select_crypto_backend }}' + register: authority_cert_issuer_sn_4 + +- name: "({{ select_crypto_backend }}) Generate CSR with authority cert issuer / serial number (remove)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_acisn.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + register: authority_cert_issuer_sn_5 + +- name: "({{ select_crypto_backend }}) Generate CSR with everything" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_everything.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject_ordered: + - commonName: www.example.com + - C: de + - L: Somewhere + - ST: Zürich + - streetAddress: Welcome Street N° 5 + - O: Ansiblé + - organizationalUnitName: Crÿpto Depârtment ☺ + - serialNumber: "1234" + - SN: Last Name Which Happens To Be A Very Løng String With A Lot Of Spaces, Jr. + - GN: First Name + - title: Chïeff + - pseudonym: test + - UID: asdf + - emailAddress: test@example.com + - postalAddress: 1234 Somewhere + - postalCode: "1234" + useCommonNameForSAN: false + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: true + extended_key_usage: '{{ value_for_extended_key_usage }}' + subject_alt_name: '{{ value_for_san }}' + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: true + name_constraints_permitted: '{{ value_for_name_constraints_permitted }}' + name_constraints_excluded: + - "DNS:.example.com" + - "DNS:.org" + name_constraints_critical: true + ocsp_must_staple: true + subject_key_identifier: 00:11:22:33 + authority_key_identifier: 44:55:66:77 + authority_cert_issuer: '{{ value_for_authority_cert_issuer }}' + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + vars: + value_for_extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + - 1.2.3.4.5.6 + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + value_for_san: + - "DNS:www.ansible.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + - "RID:1.2.3.4" + - "otherName:1.2.3.4;0c:07:63:65:72:74:72:65:71" + - "otherName:1.3.6.1.4.1.311.20.2.3;UTF8:bob@localhost" + - "dirName:CN = example.net, O = Example Net" + - "dirName:CN=example.com,O=Example Com" + value_for_name_constraints_permitted: + - "DNS:www.example.com" + - "IP:1.2.3.0/24" + - "IP:::1:0:0/112" + register: everything_1 + +- name: "({{ select_crypto_backend }}) Generate CSR with everything (idempotent, check mode)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_everything.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject_ordered: + - CN: www.example.com + - countryName: de + - L: Somewhere + - ST: Zürich + - streetAddress: Welcome Street N° 5 + - organizationName: Ansiblé + - organizationalUnitName: Crÿpto Depârtment ☺ + - serialNumber: "1234" + - SN: Last Name Which Happens To Be A Very Løng String With A Lot Of Spaces, Jr. + - GN: First Name + - title: Chïeff + - pseudonym: test + - UID: asdf + - emailAddress: test@example.com + - postalAddress: 1234 Somewhere + - postalCode: "1234" + useCommonNameForSAN: false + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: true + extended_key_usage: '{{ value_for_extended_key_usage }}' + subject_alt_name: '{{ value_for_san }}' + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: true + name_constraints_permitted: '{{ value_for_name_constraints_permitted }}' + name_constraints_excluded: + - "DNS:.org" + - "DNS:.example.com" + name_constraints_critical: true + ocsp_must_staple: true + subject_key_identifier: 00:11:22:33 + authority_key_identifier: 44:55:66:77 + authority_cert_issuer: '{{ value_for_authority_cert_issuer }}' + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + vars: + value_for_extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + - 1.2.3.4.5.6 + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + value_for_san: + - "DNS:www.ansible.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + - "RID:1.2.3.4" + - "otherName:1.2.3.4;0c:07:63:65:72:74:72:65:71" + - "otherName:1.3.6.1.4.1.311.20.2.3;UTF8:bob@localhost" + - "dirName:CN=example.net,O=Example Net" + - "dirName:CN = example.com,O = Example Com" + value_for_name_constraints_permitted: + - "DNS:www.example.com" + - "IP:1.2.3.0/255.255.255.0" + - "IP:0::0:1:0:0/112" + check_mode: true + register: everything_2 + +- name: "({{ select_crypto_backend }}) Generate CSR with everything (idempotent)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_everything.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + # Subject has been reordered, but is inside 'subject' and not 'subject_ordered' + CN: www.example.com + L: Somewhere + countryName: de + ST: Zürich + streetAddress: Welcome Street N° 5 + organizationalUnitName: Crÿpto Depârtment ☺ + organizationName: Ansiblé + serialNumber: "1234" + SN: Last Name Which Happens To Be A Very Løng String With A Lot Of Spaces, Jr. + GN: First Name + pseudonym: test + title: Chïeff + UID: asdf + emailAddress: test@example.com + postalAddress: 1234 Somewhere + postalCode: "1234" + useCommonNameForSAN: false + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: true + extended_key_usage: '{{ value_for_extended_key_usage }}' + subject_alt_name: '{{ value_for_san }}' + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: true + name_constraints_permitted: '{{ value_for_name_constraints_permitted }}' + name_constraints_excluded: + - "DNS:.org" + - "DNS:.example.com" + name_constraints_critical: true + ocsp_must_staple: true + subject_key_identifier: 00:11:22:33 + authority_key_identifier: 44:55:66:77 + authority_cert_issuer: '{{ value_for_authority_cert_issuer }}' + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + vars: + value_for_extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + - 1.2.3.4.5.6 + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + value_for_san: + - "DNS:www.ansible.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + - "RID:1.2.3.4" + - "otherName:1.2.3.4;0c:07:63:65:72:74:72:65:71" + - "otherName:1.3.6.1.4.1.311.20.2.3;UTF8:bob@localhost" + - "dirName:CN= example.net, O =Example Net" + - "dirName:/CN= example.com/O =Example Com" + value_for_name_constraints_permitted: + - "DNS:www.example.com" + - "IP:1.2.3.0/255.255.255.0" + - "IP:0::0:1:0:0/112" + register: everything_3 + +- name: "({{ select_crypto_backend }}) Generate CSR with everything (not idempotent, check mode)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_everything.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject_ordered: + # Subject has been reordered, this should force a change + - CN: www.example.com + - L: Somewhere + - countryName: de + - ST: Zürich + - streetAddress: Welcome Street N° 5 + - organizationalUnitName: Crÿpto Depârtment ☺ + - organizationName: Ansiblé + - serialNumber: "1234" + - SN: Last Name Which Happens To Be A Very Løng String With A Lot Of Spaces, Jr. + - GN: First Name + - pseudonym: test + - title: Chïeff + - UID: asdf + - emailAddress: test@example.com + - postalAddress: 1234 Somewhere + - postalCode: "1234" + useCommonNameForSAN: false + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: true + extended_key_usage: '{{ value_for_extended_key_usage }}' + subject_alt_name: '{{ value_for_san }}' + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: true + name_constraints_permitted: '{{ value_for_name_constraints_permitted }}' + name_constraints_excluded: + - "DNS:.org" + - "DNS:.example.com" + name_constraints_critical: true + ocsp_must_staple: true + subject_key_identifier: 00:11:22:33 + authority_key_identifier: 44:55:66:77 + authority_cert_issuer: '{{ value_for_authority_cert_issuer }}' + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + vars: + value_for_extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + - 1.2.3.4.5.6 + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + value_for_san: + - "DNS:www.ansible.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + - "RID:1.2.3.4" + - "otherName:1.2.3.4;0c:07:63:65:72:74:72:65:71" + - "otherName:1.3.6.1.4.1.311.20.2.3;UTF8:bob@localhost" + - "dirName:CN= example.net, O =Example Net" + - "dirName:/CN= example.com/O =Example Com" + value_for_name_constraints_permitted: + - "DNS:www.example.com" + - "IP:1.2.3.0/255.255.255.0" + - "IP:0::0:1:0:0/112" + register: everything_4 + check_mode: true + +- name: "({{ select_crypto_backend }}) Get info from CSR with everything" + community.crypto.openssl_csr_info: + path: '{{ remote_tmp_dir }}/csr_everything.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: everything_info + +- name: "({{ select_crypto_backend }}) Ed25519 and Ed448 tests (for cryptography >= 2.6)" + block: + - name: "({{ select_crypto_backend }}) Generate privatekeys" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_{{ item }}.pem' + type: '{{ item }}' + loop: + - Ed25519 + - Ed448 + register: generate_csr_ed25519_ed448_privatekey + ignore_errors: true + + - name: "({{ select_crypto_backend }}) Generate CSR if private key generation succeeded" + when: generate_csr_ed25519_ed448_privatekey is not failed + block: + + - name: "({{ select_crypto_backend }}) Generate CSR" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_{{ item }}.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: generate_csr_ed25519_ed448 + ignore_errors: true + + - name: "({{ select_crypto_backend }}) Generate CSR (idempotent)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_{{ item }}.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: generate_csr_ed25519_ed448_idempotent + ignore_errors: true + + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.6', '>=') + +- name: "({{ select_crypto_backend }}) CRL distribution endpoints (for cryptography >= 1.6)" + block: + - name: "({{ select_crypto_backend }}) Create CSR with CRL distribution endpoints" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_crl_d_e.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + crl_distribution_points: + - full_name: + - "URI:https://ca.example.com/revocations.crl" + crl_issuer: + - "URI:https://ca.example.com/" + reasons: + - key_compromise + - ca_compromise + - cessation_of_operation + - relative_name: + - CN=ca.example.com + reasons: + - certificate_hold + select_crypto_backend: '{{ select_crypto_backend }}' + register: crl_distribution_endpoints_1 + + - name: "({{ select_crypto_backend }}) Create CSR with CRL distribution endpoints (idempotence)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_crl_d_e.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + crl_distribution_points: + - full_name: + - "URI:https://ca.example.com/revocations.crl" + crl_issuer: + - "URI:https://ca.example.com/" + reasons: + - key_compromise + - ca_compromise + - cessation_of_operation + - relative_name: + - CN=ca.example.com + reasons: + - certificate_hold + select_crypto_backend: '{{ select_crypto_backend }}' + register: crl_distribution_endpoints_2 + + - name: "({{ select_crypto_backend }}) Create CSR with CRL distribution endpoints (change)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_crl_d_e.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + crl_distribution_points: + - crl_issuer: + - "URI:https://ca.example.com/" + reasons: + - key_compromise + - ca_compromise + - cessation_of_operation + - relative_name: + - CN=ca.example.com + reasons: + - certificate_hold + select_crypto_backend: '{{ select_crypto_backend }}' + register: crl_distribution_endpoints_3 + + - name: "({{ select_crypto_backend }}) Create CSR with CRL distribution endpoints (no endpoints)" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_crl_d_e.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + register: crl_distribution_endpoints_4 + + - name: "({{ select_crypto_backend }}) Create CSR with CRL distribution endpoints" + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_crl_d_e.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + crl_distribution_points: + - full_name: + - "URI:https://ca.example.com/revocations.crl" + select_crypto_backend: '{{ select_crypto_backend }}' + register: crl_distribution_endpoints_5 + + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.6', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/tasks/main.yml new file mode 100644 index 000000000..cd68e9153 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/tasks/main.yml @@ -0,0 +1,32 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- block: + - name: Prepare private key for backend autodetection test + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_backend_selection.pem' + size: '{{ default_rsa_key_size }}' + - name: Run module with backend autodetection + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_backend_selection.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_backend_selection.pem' + subject: + commonName: www.ansible.com + + - 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.3', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/tests/validate.yml new file mode 100644 index 000000000..0a02a86d5 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr/tests/validate.yml @@ -0,0 +1,346 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "({{ select_crypto_backend }}) Validate CSR (test - privatekey modulus)" + shell: '{{ openssl_binary }} rsa -noout -modulus -in {{ remote_tmp_dir }}/privatekey.pem' + register: privatekey_modulus + +- name: "({{ select_crypto_backend }}) Validate CSR (test - Common Name)" + shell: "{{ openssl_binary }} req -noout -subject -in {{ remote_tmp_dir }}/csr.csr -nameopt oneline,-space_eq" + register: csr_cn + +- name: "({{ select_crypto_backend }}) Validate CSR (test - csr modulus)" + shell: '{{ openssl_binary }} req -noout -modulus -in {{ remote_tmp_dir }}/csr.csr' + register: csr_modulus + +- name: "({{ select_crypto_backend }}) Validate CSR (assert)" + assert: + that: + - csr_cn.stdout.split('=')[-1] == 'www.ansible.com' + - csr_modulus.stdout == privatekey_modulus.stdout + +- name: "({{ select_crypto_backend }}) Validate CSR (check mode, idempotency)" + assert: + that: + - generate_csr_check is changed + - generate_csr is changed + - generate_csr_idempotent is not changed + - generate_csr_idempotent_check is not changed + +- name: "({{ select_crypto_backend }}) Read CSR" + slurp: + src: '{{ remote_tmp_dir }}/csr.csr' + register: slurp + +- name: "({{ select_crypto_backend }}) Validate CSR (data retrieval)" + assert: + that: + - generate_csr_check.csr is none + - generate_csr.csr == (slurp.content | b64decode) + - generate_csr.csr == generate_csr_idempotent.csr + - generate_csr.csr == generate_csr_idempotent_check.csr + +- name: "({{ select_crypto_backend }}) Validate CSR without SAN (check mode, idempotency)" + assert: + that: + - generate_csr_nosan_check is changed + - generate_csr_nosan is changed + - generate_csr_nosan_check_idempotent is not changed + - generate_csr_nosan_check_idempotent_check is not changed + +- name: "({{ select_crypto_backend }}) Validate CSR_KU_XKU (assert idempotency, change)" + assert: + that: + - csr_ku_xku is not changed + - csr_ku_xku_change is changed + - csr_ku_xku_change_2 is changed + +- name: "({{ select_crypto_backend }}) Validate old_API CSR (test - Common Name)" + shell: "{{ openssl_binary }} req -noout -subject -in {{ remote_tmp_dir }}/csr_oldapi.csr -nameopt oneline,-space_eq" + register: csr_oldapi_cn + +- name: "({{ select_crypto_backend }}) Validate old_API CSR (test - csr modulus)" + shell: '{{ openssl_binary }} req -noout -modulus -in {{ remote_tmp_dir }}/csr_oldapi.csr' + register: csr_oldapi_modulus + +- name: "({{ select_crypto_backend }}) Validate old_API CSR (assert)" + assert: + that: + - csr_oldapi_cn.stdout.split('=')[-1] == 'www.ansible.com' + - csr_oldapi_modulus.stdout == privatekey_modulus.stdout + +- name: "({{ select_crypto_backend }}) Validate invalid SAN (1/2)" + assert: + that: + - generate_csr_invalid_san is failed + - "'Subject Alternative Name' in generate_csr_invalid_san.msg" + +- name: "({{ select_crypto_backend }}) Validate invalid SAN (2/2)" + # Note that modern cryptography versions simply accept this name. + # The error has been observed with cryptography 1.7.2 and 1.9, but not with 2.3 and newer. + assert: + that: + - generate_csr_invalid_san_2 is failed + - "'The label system:kube-controller-manager is not a valid A-label' in generate_csr_invalid_san_2.msg" + when: cryptography_version.stdout is version('2.0', '<') + +- name: "({{ select_crypto_backend }}) Validate OCSP Must Staple CSR (test - everything)" + shell: "{{ openssl_binary }} req -noout -in {{ remote_tmp_dir }}/csr_ocsp.csr -text" + register: csr_ocsp + +- name: "({{ select_crypto_backend }}) Validate OCSP Must Staple CSR (assert)" + assert: + that: + - "(csr_ocsp.stdout is search('\\s+TLS Feature:\\s*\\n\\s+status_request\\s+')) or + (csr_ocsp.stdout is search('\\s+1.3.6.1.5.5.7.1.24:\\s*\\n\\s+0\\.\\.\\.\\.\\s+'))" + +- name: "({{ select_crypto_backend }}) Validate OCSP Must Staple CSR (assert idempotency)" + assert: + that: + - csr_ocsp_idempotency is not changed + +- name: "({{ select_crypto_backend }}) Validate ECC CSR (test - privatekey's public key)" + shell: '{{ openssl_binary }} ec -pubout -in {{ remote_tmp_dir }}/privatekey2.pem' + register: privatekey_ecc_key + +- name: "({{ select_crypto_backend }}) Validate ECC CSR (test - Common Name)" + shell: "{{ openssl_binary }} req -noout -subject -in {{ remote_tmp_dir }}/csr2.csr -nameopt oneline,-space_eq" + register: csr_ecc_cn + +- name: "({{ select_crypto_backend }}) Validate ECC CSR (test - CSR pubkey)" + shell: '{{ openssl_binary }} req -noout -pubkey -in {{ remote_tmp_dir }}/csr2.csr' + register: csr_ecc_pubkey + +- name: "({{ select_crypto_backend }}) Validate ECC CSR (assert)" + assert: + that: + - csr_ecc_cn.stdout.split('=')[-1] == 'www.ansible.com' + - csr_ecc_pubkey.stdout == privatekey_ecc_key.stdout + +- name: "({{ select_crypto_backend }}) Validate CSR (text common name - Common Name)" + shell: "{{ openssl_binary }} req -noout -subject -in {{ remote_tmp_dir }}/csr3.csr -nameopt oneline,-space_eq" + register: csr3_cn + +- name: "({{ select_crypto_backend }}) Validate CSR (assert)" + assert: + that: + - csr3_cn.stdout.split('=')[-1] == 'This is for Ansible' + +- name: "({{ select_crypto_backend }}) Validate country name idempotency and validation" + assert: + that: + - country_idempotent_1 is changed + - country_idempotent_2 is not changed + - country_idempotent_3 is not changed + - country_fail_4 is failed + +- name: "({{ select_crypto_backend }}) Validate idempotency of privatekey_passphrase" + assert: + that: + - passphrase_1 is changed + - passphrase_1_content is not changed + +- name: "({{ select_crypto_backend }}) Validate private key passphrase errors" + assert: + that: + - passphrase_error_1 is failed + - "'assphrase' in passphrase_error_1.msg or 'assword' in passphrase_error_1.msg" + - passphrase_error_2 is failed + - "'assphrase' in passphrase_error_2.msg or 'assword' in passphrase_error_2.msg or 'serializ' in passphrase_error_2.msg" + - passphrase_error_3 is failed + - "'assphrase' in passphrase_error_3.msg or 'assword' in passphrase_error_3.msg or 'serializ' in passphrase_error_3.msg" + +- name: "({{ select_crypto_backend }}) Verify that broken CSR will be regenerated" + assert: + that: + - output_broken is changed + +- name: "({{ select_crypto_backend }}) Verify that subject key identifier handling works" + assert: + that: + - subject_key_identifier_1 is changed + - subject_key_identifier_2 is not changed + - subject_key_identifier_3 is changed + - subject_key_identifier_4 is changed + - subject_key_identifier_5 is not changed + - subject_key_identifier_6 is changed + +- name: "({{ select_crypto_backend }}) Verify that authority key identifier handling works" + assert: + that: + - authority_key_identifier_1 is changed + - authority_key_identifier_2 is not changed + - authority_key_identifier_3 is changed + - authority_key_identifier_4 is changed + +- name: "({{ select_crypto_backend }}) Verify that authority cert issuer / serial number handling works" + assert: + that: + - authority_cert_issuer_sn_1 is changed + - authority_cert_issuer_sn_2 is not changed + - authority_cert_issuer_sn_3 is changed + - authority_cert_issuer_sn_4 is changed + - authority_cert_issuer_sn_5 is changed + +- name: "({{ select_crypto_backend }}) Check backup" + assert: + that: + - csr_backup_1 is changed + - csr_backup_1.backup_file is undefined + - csr_backup_2 is not changed + - csr_backup_2.backup_file is undefined + - csr_backup_3 is changed + - csr_backup_3.backup_file is string + - csr_backup_4 is changed + - csr_backup_4.backup_file is string + - csr_backup_5 is not changed + - csr_backup_5.backup_file is undefined + - csr_backup_4.csr is none + +- name: "({{ select_crypto_backend }}) Check CSR with everything" + assert: + that: + - everything_1 is changed + - everything_2 is not changed + - everything_3 is not changed + - everything_4 is changed + - everything_info.basic_constraints == [ + "CA:TRUE", + "pathlen:23", + ] + - everything_info.basic_constraints_critical == true + - everything_info.extended_key_usage_critical == false + - everything_info.key_usage == [ + "CRL Sign", + "Certificate Sign", + "Data Encipherment", + "Decipher Only", + "Digital Signature", + "Encipher Only", + "Key Agreement", + "Key Encipherment", + "Non Repudiation" + ] + - everything_info.key_usage_critical == true + - everything_info.ocsp_must_staple == true + - everything_info.ocsp_must_staple_critical == false + - everything_info.signature_valid == true + - everything_info.subject.commonName == "www.example.com" + - everything_info.subject.countryName == "de" + - everything_info.subject.emailAddress == "test@example.com" + - everything_info.subject.givenName == "First Name" + - everything_info.subject.localityName == "Somewhere" + - everything_info.subject.organizationName == "Ansiblé" + - everything_info.subject.organizationalUnitName == "Crÿpto Depârtment ☺" + - everything_info.subject.postalAddress == "1234 Somewhere" + - everything_info.subject.postalCode == "1234" + - everything_info.subject.pseudonym == "test" + - everything_info.subject.serialNumber == "1234" + - everything_info.subject.stateOrProvinceName == "Zürich" + - everything_info.subject.streetAddress == "Welcome Street N° 5" + - everything_info.subject.surname == "Last Name Which Happens To Be A Very Løng String With A Lot Of Spaces, Jr." + - everything_info.subject.title == "Chïeff" + - everything_info.subject.userId == "asdf" + - everything_info.subject | length == 16 + - > + everything_info.subject_ordered == [ + ["commonName", "www.example.com"], + ["countryName", "de"], + ["localityName", "Somewhere"], + ["stateOrProvinceName", "Zürich"], + ["streetAddress", "Welcome Street N° 5"], + ["organizationName", "Ansiblé"], + ["organizationalUnitName", "Crÿpto Depârtment ☺"], + ["serialNumber", "1234"], + ["surname", "Last Name Which Happens To Be A Very Løng String With A Lot Of Spaces, Jr."], + ["givenName", "First Name"], + ["title", "Chïeff"], + ["pseudonym", "test"], + ["userId", "asdf"], + ["emailAddress", "test@example.com"], + ["postalAddress", "1234 Somewhere"], + ["postalCode", "1234"], + ] + - everything_info.subject_alt_name_critical == false + - everything_info.name_constraints_excluded == [ + "DNS:.example.com", + "DNS:.org", + ] + - everything_info.name_constraints_critical == true + +- name: "({{ select_crypto_backend }}) Check CSR with everything" + assert: + that: + - everything_info.authority_cert_issuer == [ + "DNS:ca.example.org", + "IP:1.2.3.4" + ] + - everything_info.authority_cert_serial_number == 12345 + - everything_info.authority_key_identifier == "44:55:66:77" + - everything_info.subject_alt_name == [ + "DNS:www.ansible.com", + "IP:1.2.3.4", + "IP:::1", + "email:test@example.org", + "URI:https://example.org/test/index.html", + "RID:1.2.3.4", + "otherName:1.2.3.4;0c:07:63:65:72:74:72:65:71", + "otherName:1.3.6.1.4.1.311.20.2.3;0c:0d:62:6f:62:40:6c:6f:63:61:6c:68:6f:73:74", + "dirName:CN=example.net,O=Example Net", + "dirName:CN=example.com,O=Example Com" + ] + - everything_info.subject_key_identifier == "00:11:22:33" + - everything_info.extended_key_usage == [ + "1.2.3.4.5.6", + "Any Extended Key Usage", + "Biometric Info", + "Code Signing", + "E-mail Protection", + "IPSec User", + "OCSP Signing", + "TLS Web Client Authentication", + "TLS Web Server Authentication", + "TLS Web Server Authentication", + "Time Stamping", + "dvcs", + "qcStatements", + ] + - everything_info.name_constraints_permitted == [ + "DNS:www.example.com", + "IP:1.2.3.0/24", + "IP:::1:0:0/112", + ] + +- name: "({{ select_crypto_backend }}) Verify Ed25519 and Ed448 tests (for cryptography >= 2.6, < 2.8)" + assert: + that: + - generate_csr_ed25519_ed448.results[0] is failed + - generate_csr_ed25519_ed448.results[1] is failed + - generate_csr_ed25519_ed448.results[0].msg == 'Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.' + - generate_csr_ed25519_ed448.results[1].msg == 'Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.' + - generate_csr_ed25519_ed448_idempotent.results[0] is failed + - generate_csr_ed25519_ed448_idempotent.results[1] is failed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.6', '>=') and cryptography_version.stdout is version('2.8', '<') and generate_csr_ed25519_ed448_privatekey is not failed + +- name: "({{ select_crypto_backend }}) Verify Ed25519 and Ed448 tests (for cryptography >= 2.8)" + assert: + that: + - generate_csr_ed25519_ed448 is succeeded + - generate_csr_ed25519_ed448.results[0] is changed + - generate_csr_ed25519_ed448.results[1] is changed + - generate_csr_ed25519_ed448_idempotent is succeeded + - generate_csr_ed25519_ed448_idempotent.results[0] is not changed + - generate_csr_ed25519_ed448_idempotent.results[1] is not changed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.8', '>=') and generate_csr_ed25519_ed448_privatekey is not failed + +- name: "({{ select_crypto_backend }}) Verify CRL distribution endpoints (for cryptography >= 1.6)" + assert: + that: + - crl_distribution_endpoints_1 is changed + - crl_distribution_endpoints_2 is not changed + - crl_distribution_endpoints_3 is changed + - crl_distribution_endpoints_4 is changed + - crl_distribution_endpoints_5 is changed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.6', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_info/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_info/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_info/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_info/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_info/meta/main.yml new file mode 100644 index 000000000..7c2b42405 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_info/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir + - prepare_jinja2_compat diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_info/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_info/tasks/impl.yml new file mode 100644 index 000000000..0311d27c5 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_info/tasks/impl.yml @@ -0,0 +1,125 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- debug: + msg: "Executing tests with backend {{ select_crypto_backend }}" + +- name: "({{ select_crypto_backend }}) Get CSR info" + openssl_csr_info: + path: '{{ remote_tmp_dir }}/csr_1.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: "({{ select_crypto_backend }}) Get CSR info (IDNA encoding)" + openssl_csr_info: + path: '{{ remote_tmp_dir }}/csr_1.csr' + name_encoding: idna + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_idna + +- name: "({{ select_crypto_backend }}) Get CSR info (Unicode encoding)" + openssl_csr_info: + path: '{{ remote_tmp_dir }}/csr_1.csr' + name_encoding: unicode + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_unicode + +- name: "({{ select_crypto_backend }}) Check whether subject and extensions behaves as expected" + assert: + that: + - result.subject.organizationalUnitName == 'ACME Department' + - "['organizationalUnitName', 'Crypto Department'] in result.subject_ordered" + - "['organizationalUnitName', 'ACME Department'] in result.subject_ordered" + - result.public_key_type == 'RSA' + - result.public_key_data.size == default_rsa_key_size + # TLS Feature + - result.extensions_by_oid['1.3.6.1.5.5.7.1.24'].critical == false + - result.extensions_by_oid['1.3.6.1.5.5.7.1.24'].value == 'MAMCAQU=' + # Key Usage + - result.extensions_by_oid['2.5.29.15'].critical == true + - result.extensions_by_oid['2.5.29.15'].value in ['AwMA/4A=', 'AwMH/4A='] + # Subject Alternative Names + - result.subject_alt_name[1] == ("DNS:âņsïbłè.com" if cryptography_version.stdout is version('2.1', '<') else "DNS:xn--sb-oia0a7a53bya.com") + - result_unicode.subject_alt_name[1] == "DNS:âņsïbłè.com" + - result_idna.subject_alt_name[1] == "DNS:xn--sb-oia0a7a53bya.com" + - result.extensions_by_oid['2.5.29.17'].critical == false + - result.extensions_by_oid['2.5.29.17'].value == 'MHmCD3d3dy5hbnNpYmxlLmNvbYIXeG4tLXNiLW9pYTBhN2E1M2J5YS5jb22HBAECAwSHEAAAAAAAAAAAAAAAAAAAAAGBEHRlc3RAZXhhbXBsZS5vcmeGI2h0dHBzOi8vZXhhbXBsZS5vcmcvdGVzdC9pbmRleC5odG1s' + # Basic Constraints + - result.extensions_by_oid['2.5.29.19'].critical == true + - result.extensions_by_oid['2.5.29.19'].value == 'MAYBAf8CARc=' + # Extended Key Usage + - result.extensions_by_oid['2.5.29.37'].critical == false + - result.extensions_by_oid['2.5.29.37'].value == 'MHQGCCsGAQUFBwMBBggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUFBwMJBgRVHSUABggrBgEFBQcBAwYIKwYBBQUHAwoGCCsGAQUFBwMHBggrBgEFBQcBAg==' + +- name: "({{ select_crypto_backend }}) Check SubjectKeyIdentifier and AuthorityKeyIdentifier" + assert: + that: + - result.subject_key_identifier == "00:11:22:33" + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + # Subject Key Identifier + - result.extensions_by_oid['2.5.29.14'].critical == false + # Authority Key Identifier + - result.extensions_by_oid['2.5.29.35'].critical == false + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: cryptography_version.stdout is version('1.3', '>=') + +- name: "({{ select_crypto_backend }}) Read CSR" + slurp: + src: '{{ remote_tmp_dir }}/csr_1.csr' + register: slurp + +- name: "({{ select_crypto_backend }}) Get CSR info directly" + openssl_csr_info: + content: '{{ slurp.content | b64decode }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_direct + +- name: "({{ select_crypto_backend }}) Compare output of direct and loaded info" + assert: + that: + - result == result_direct + +- name: "({{ select_crypto_backend }}) Get CSR info" + openssl_csr_info: + path: '{{ remote_tmp_dir }}/csr_2.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: "({{ select_crypto_backend }}) Get CSR info" + openssl_csr_info: + path: '{{ remote_tmp_dir }}/csr_3.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: "({{ select_crypto_backend }}) Check AuthorityKeyIdentifier" + assert: + that: + - result.authority_key_identifier is none + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: cryptography_version.stdout is version('1.3', '>=') + +- name: "({{ select_crypto_backend }}) Get CSR info" + openssl_csr_info: + path: '{{ remote_tmp_dir }}/csr_4.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: "({{ select_crypto_backend }}) Check AuthorityKeyIdentifier" + assert: + that: + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer is none + - result.authority_cert_serial_number is none + when: cryptography_version.stdout is version('1.3', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_info/tasks/main.yml new file mode 100644 index 000000000..05ffbc512 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_info/tasks/main.yml @@ -0,0 +1,136 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Make sure the Python idna library is installed + pip: + name: idna + state: present + +- name: Generate privatekey + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey.pem' + size: '{{ default_rsa_key_size }}' + +- name: Generate privatekey with password + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + size: '{{ default_rsa_key_size }}' + +- name: Generate CSR 1 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_1.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.example.com + C: de + L: Somewhere + ST: Zurich + streetAddress: Welcome Street + O: Ansible + organizationalUnitName: + - Crypto Department + - ACME Department + serialNumber: "1234" + SN: Last Name + GN: First Name + title: Chief + pseudonym: test + UID: asdf + emailAddress: test@example.com + postalAddress: 1234 Somewhere + postalCode: "1234" + useCommonNameForSAN: false + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: true + extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + subject_alt_name: + - "DNS:www.ansible.com" + - "DNS:âņsïbłè.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: true + ocsp_must_staple: true + subject_key_identifier: '{{ "00:11:22:33" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + +- name: Generate CSR 2 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_2.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + privatekey_passphrase: hunter2 + useCommonNameForSAN: false + basic_constraints: + - "CA:TRUE" + +- name: Generate CSR 3 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_3.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + useCommonNameForSAN: false + subject_alt_name: + - "DNS:*.ansible.com" + - "DNS:*.example.org" + - "IP:DEAD:BEEF::1" + basic_constraints: + - "CA:FALSE" + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + +- name: Generate CSR 4 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_4.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + useCommonNameForSAN: false + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + +- name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + when: cryptography_version.stdout is version('1.3', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_pipe/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_pipe/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_pipe/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_pipe/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_pipe/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_pipe/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_pipe/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_pipe/tasks/impl.yml new file mode 100644 index 000000000..adf1836b2 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_pipe/tasks/impl.yml @@ -0,0 +1,96 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "({{ select_crypto_backend }}) Generate privatekey" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey.pem' + size: '{{ default_rsa_key_size }}' + +- name: "({{ select_crypto_backend }}) Generate CSR (check mode)" + openssl_csr_pipe: + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: generate_csr_check + +- name: "({{ select_crypto_backend }}) Generate CSR" + openssl_csr_pipe: + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_csr + +- name: "({{ select_crypto_backend }}) Generate CSR (idempotent)" + openssl_csr_pipe: + content: "{{ generate_csr.csr }}" + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_csr_idempotent + +- name: "({{ select_crypto_backend }}) Generate CSR (idempotent, check mode)" + openssl_csr_pipe: + content: "{{ generate_csr.csr }}" + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: generate_csr_idempotent_check + +- name: "({{ select_crypto_backend }}) Generate CSR (changed)" + openssl_csr_pipe: + content: "{{ generate_csr.csr }}" + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_csr_changed + +- name: "({{ select_crypto_backend }}) Generate CSR (changed, check mode)" + openssl_csr_pipe: + content: "{{ generate_csr.csr }}" + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: generate_csr_changed_check + +- name: "({{ select_crypto_backend }}) Validate CSR (test - privatekey modulus)" + shell: '{{ openssl_binary }} rsa -noout -modulus -in {{ remote_tmp_dir }}/privatekey.pem' + register: privatekey_modulus + +- name: "({{ select_crypto_backend }}) Validate CSR (test - Common Name)" + shell: "{{ openssl_binary }} req -noout -subject -in /dev/stdin -nameopt oneline,-space_eq" + args: + stdin: "{{ generate_csr.csr }}" + register: csr_cn + +- name: "({{ select_crypto_backend }}) Validate CSR (test - csr modulus)" + shell: '{{ openssl_binary }} req -noout -modulus -in /dev/stdin' + args: + stdin: "{{ generate_csr.csr }}" + register: csr_modulus + +- name: "({{ select_crypto_backend }}) Validate CSR (assert)" + assert: + that: + - csr_cn.stdout.split('=')[-1] == 'www.ansible.com' + - csr_modulus.stdout == privatekey_modulus.stdout + +- name: "({{ select_crypto_backend }}) Validate CSR (check mode, idempotency)" + assert: + that: + - generate_csr_check is changed + - generate_csr is changed + - generate_csr_idempotent is not changed + - generate_csr_idempotent_check is not changed + - generate_csr_changed is changed + - generate_csr_changed_check is changed diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_pipe/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_pipe/tasks/main.yml new file mode 100644 index 000000000..ecf238d72 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_csr_pipe/tasks/main.yml @@ -0,0 +1,27 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Prepare private key for backend autodetection test + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_backend_selection.pem' + size: '{{ default_rsa_key_size }}' +- name: Run module with backend autodetection + openssl_csr_pipe: + privatekey_path: '{{ remote_tmp_dir }}/privatekey_backend_selection.pem' + subject: + commonName: www.ansible.com + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('1.3', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/tasks/impl.yml new file mode 100644 index 000000000..85886e83e --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/tasks/impl.yml @@ -0,0 +1,123 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# The tests for this module generate unsafe parameters for testing purposes; +# otherwise tests would be too slow. Use sizes of at least 2048 in production! +- name: "[{{ select_crypto_backend }}] Generate parameter (check mode)" + openssl_dhparam: + size: 768 + path: '{{ remote_tmp_dir }}/dh768.pem' + select_crypto_backend: "{{ select_crypto_backend }}" + return_content: true + check_mode: true + register: dhparam_check + +- name: "[{{ select_crypto_backend }}] Generate parameter" + openssl_dhparam: + size: 768 + path: '{{ remote_tmp_dir }}/dh768.pem' + select_crypto_backend: "{{ select_crypto_backend }}" + return_content: true + register: dhparam + +- name: "[{{ select_crypto_backend }}] Don't regenerate parameters with no change (check mode)" + openssl_dhparam: + size: 768 + path: '{{ remote_tmp_dir }}/dh768.pem' + select_crypto_backend: "{{ select_crypto_backend }}" + return_content: true + check_mode: true + register: dhparam_changed_check + +- name: "[{{ select_crypto_backend }}] Don't regenerate parameters with no change" + openssl_dhparam: + size: 768 + path: '{{ remote_tmp_dir }}/dh768.pem' + select_crypto_backend: "{{ select_crypto_backend }}" + return_content: true + register: dhparam_changed + +- name: "[{{ select_crypto_backend }}] Generate parameters with size option" + openssl_dhparam: + path: '{{ remote_tmp_dir }}/dh512.pem' + size: 512 + select_crypto_backend: "{{ select_crypto_backend }}" + +- name: "[{{ select_crypto_backend }}] Don't regenerate parameters with size option and no change" + openssl_dhparam: + path: '{{ remote_tmp_dir }}/dh512.pem' + size: 512 + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_changed_512 + +- copy: + src: '{{ remote_tmp_dir }}/dh768.pem' + remote_src: true + dest: '{{ remote_tmp_dir }}/dh512.pem' + +- name: "[{{ select_crypto_backend }}] Re-generate if size is different" + openssl_dhparam: + path: '{{ remote_tmp_dir }}/dh512.pem' + size: 512 + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_changed_to_512 + +- name: "[{{ select_crypto_backend }}] Force re-generate parameters with size option" + openssl_dhparam: + path: '{{ remote_tmp_dir }}/dh512.pem' + size: 512 + force: true + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_changed_force + +- name: "[{{ select_crypto_backend }}] Create broken params" + copy: + dest: "{{ remote_tmp_dir }}/dhbroken.pem" + content: "broken" +- name: "[{{ select_crypto_backend }}] Regenerate broken params" + openssl_dhparam: + path: '{{ remote_tmp_dir }}/dhbroken.pem' + size: 512 + force: true + select_crypto_backend: "{{ select_crypto_backend }}" + register: output_broken + +- name: "[{{ select_crypto_backend }}] Generate params" + openssl_dhparam: + path: '{{ remote_tmp_dir }}/dh_backup.pem' + size: 512 + backup: true + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_backup_1 +- name: "[{{ select_crypto_backend }}] Generate params (idempotent)" + openssl_dhparam: + path: '{{ remote_tmp_dir }}/dh_backup.pem' + size: 512 + backup: true + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_backup_2 +- name: "[{{ select_crypto_backend }}] Generate params (change)" + openssl_dhparam: + path: '{{ remote_tmp_dir }}/dh_backup.pem' + size: 512 + force: true + backup: true + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_backup_3 +- name: "[{{ select_crypto_backend }}] Generate params (remove)" + openssl_dhparam: + path: '{{ remote_tmp_dir }}/dh_backup.pem' + state: absent + backup: true + select_crypto_backend: "{{ select_crypto_backend }}" + return_content: true + register: dhparam_backup_4 +- name: "[{{ select_crypto_backend }}] Generate params (remove, idempotent)" + openssl_dhparam: + path: '{{ remote_tmp_dir }}/dh_backup.pem' + state: absent + backup: true + select_crypto_backend: "{{ select_crypto_backend }}" + register: dhparam_backup_5 diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/tasks/main.yml new file mode 100644 index 000000000..e68169e5f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/tasks/main.yml @@ -0,0 +1,47 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# The tests for this module generate unsafe parameters for testing purposes; +# otherwise tests would be too slow. Use sizes of at least 2048 in production! + +- name: Run module with backend autodetection + openssl_dhparam: + path: '{{ remote_tmp_dir }}/dh_backend_selection.pem' + size: 512 + +- block: + - name: Running tests with OpenSSL backend + include_tasks: impl.yml + + - include_tasks: ../tests/validate.yml + + vars: + select_crypto_backend: openssl + # when: openssl_version.stdout is version('1.0.0', '>=') + +- name: Remove output directory + file: + path: "{{ remote_tmp_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ remote_tmp_dir }}" + state: directory + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + + - include_tasks: ../tests/validate.yml + + vars: + select_crypto_backend: cryptography + when: cryptography_version.stdout is version('2.0', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/tests/validate.yml new file mode 100644 index 000000000..37e68d72d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_dhparam/tests/validate.yml @@ -0,0 +1,70 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "[{{ select_crypto_backend }}] Validate generated params" + shell: '{{ openssl_binary }} dhparam -in {{ remote_tmp_dir }}/{{ item }}.pem -noout -check' + with_items: + - dh768 + - dh512 + +- name: "[{{ select_crypto_backend }}] Get bit size of 768" + shell: '{{ openssl_binary }} dhparam -noout -in {{ remote_tmp_dir }}/dh768.pem -text | head -n1 | sed -ne "s@.*(\\([[:digit:]]\{1,\}\\) bit).*@\\1@p"' + register: bit_size_dhparam + +- name: "[{{ select_crypto_backend }}] Check bit size of default" + assert: + that: + - bit_size_dhparam.stdout == "768" + +- name: "[{{ select_crypto_backend }}] Get bit size of 512" + shell: '{{ openssl_binary }} dhparam -noout -in {{ remote_tmp_dir }}/dh512.pem -text | head -n1 | sed -ne "s@.*(\\([[:digit:]]\{1,\}\\) bit).*@\\1@p"' + register: bit_size_dhparam_512 + +- name: "[{{ select_crypto_backend }}] Check bit size of default" + assert: + that: + - bit_size_dhparam_512.stdout == "512" + +- name: "[{{ select_crypto_backend }}] Check if changed works correctly" + assert: + that: + - dhparam_check is changed + - dhparam is changed + - dhparam_changed_check is not changed + - dhparam_changed is not changed + - dhparam_changed_512 is not changed + - dhparam_changed_to_512 is changed + - dhparam_changed_force is changed + +- name: "[{{ select_crypto_backend }}] Read result" + slurp: + src: '{{ remote_tmp_dir }}/dh768.pem' + register: slurp + +- name: "[{{ select_crypto_backend }}] Make sure correct values are returned" + assert: + that: + - dhparam.dhparams == (slurp.content | b64decode) + - dhparam.dhparams == dhparam_changed.dhparams + +- name: "[{{ select_crypto_backend }}] Verify that broken params will be regenerated" + assert: + that: + - output_broken is changed + +- name: "[{{ select_crypto_backend }}] Check backup" + assert: + that: + - dhparam_backup_1 is changed + - dhparam_backup_1.backup_file is undefined + - dhparam_backup_2 is not changed + - dhparam_backup_2.backup_file is undefined + - dhparam_backup_3 is changed + - dhparam_backup_3.backup_file is string + - dhparam_backup_4 is changed + - dhparam_backup_4.backup_file is string + - dhparam_backup_5 is not changed + - dhparam_backup_5.backup_file is undefined + - dhparam_backup_4.dhparams is none diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/meta/main.yml new file mode 100644 index 000000000..26fa5f7da --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_pyopenssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/tasks/impl.yml new file mode 100644 index 000000000..c2bc6adae --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/tasks/impl.yml @@ -0,0 +1,367 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- block: + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (check mode)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + return_content: true + check_mode: true + register: p12_standard_check + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + return_content: true + register: p12_standard + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file again, idempotency (check mode)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + return_content: true + check_mode: true + register: p12_standard_idempotency_check + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file again, idempotency" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + return_content: true + register: p12_standard_idempotency + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file again, idempotency (empty other_certificates)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + return_content: true + other_certificates: [] + register: p12_standard_idempotency_no_certs + + - name: "({{ select_crypto_backend }}) Read ansible_pkey1.pem" + slurp: + src: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + register: ansible_pkey_content + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file again, idempotency (private key from file)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_content: '{{ ansible_pkey_content.content | b64decode }}' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + return_content: true + register: p12_standard_idempotency_2 + + - name: "({{ select_crypto_backend }}) Read ansible.p12" + slurp: + src: '{{ remote_tmp_dir }}/ansible.p12' + register: ansible_p12_content + + - name: "({{ select_crypto_backend }}) Validate PKCS#12" + assert: + that: + - p12_standard.pkcs12 == ansible_p12_content.content + - p12_standard_idempotency.pkcs12 == p12_standard.pkcs12 + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (force)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + force: true + register: p12_force + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (force + change mode)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + force: true + mode: '0644' + register: p12_force_and_mode + + - name: "({{ select_crypto_backend }}) Dump PKCS#12" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + src: '{{ remote_tmp_dir }}/ansible.p12' + path: '{{ remote_tmp_dir }}/ansible_parse.pem' + action: parse + state: present + register: p12_dumped + + - name: "({{ select_crypto_backend }}) Dump PKCS#12 file again, idempotency" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + src: '{{ remote_tmp_dir }}/ansible.p12' + path: '{{ remote_tmp_dir }}/ansible_parse.pem' + action: parse + state: present + register: p12_dumped_idempotency + + - name: "({{ select_crypto_backend }}) Dump PKCS#12, check mode" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + src: '{{ remote_tmp_dir }}/ansible.p12' + path: '{{ remote_tmp_dir }}/ansible_parse.pem' + action: parse + state: present + check_mode: true + register: p12_dumped_check_mode + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file with multiple certs and passphrase" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_multi_certs.p12' + friendly_name: abracadabra + passphrase: hunter3 + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + other_certificates: + - '{{ remote_tmp_dir }}/ansible2.crt' + - '{{ remote_tmp_dir }}/ansible3.crt' + state: present + register: p12_multiple_certs + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file with multiple certs and passphrase, again (idempotency)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_multi_certs.p12' + friendly_name: abracadabra + passphrase: hunter3 + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + other_certificates: + - '{{ remote_tmp_dir }}/ansible2.crt' + - '{{ remote_tmp_dir }}/ansible3.crt' + state: present + register: p12_multiple_certs_idempotency + + - name: "({{ select_crypto_backend }}) Dump PKCS#12 with multiple certs and passphrase" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + src: '{{ remote_tmp_dir }}/ansible_multi_certs.p12' + path: '{{ remote_tmp_dir }}/ansible_parse_multi_certs.pem' + passphrase: hunter3 + action: parse + state: present + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (password fail 1)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_pw1.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + privatekey_passphrase: hunter2 + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + ignore_errors: true + register: passphrase_error_1 + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (password fail 2)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_pw2.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + privatekey_passphrase: wrong_password + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + ignore_errors: true + register: passphrase_error_2 + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (password fail 3)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_pw3.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + ignore_errors: true + register: passphrase_error_3 + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file, no privatekey" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_no_pkey.p12' + friendly_name: abracadabra + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + register: p12_no_pkey + + - name: "({{ select_crypto_backend }}) Create broken PKCS#12" + copy: + dest: '{{ remote_tmp_dir }}/broken.p12' + content: broken + + - name: "({{ select_crypto_backend }}) Regenerate broken PKCS#12" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/broken.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + force: true + mode: '0644' + register: output_broken + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_backup.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + backup: true + register: p12_backup_1 + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (idempotent)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_backup.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + backup: true + register: p12_backup_2 + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (change)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_backup.p12' + friendly_name: abra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + force: true + backup: true + register: p12_backup_3 + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (remove)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_backup.p12' + state: absent + backup: true + return_content: true + register: p12_backup_4 + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (remove, idempotent)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_backup.p12' + state: absent + backup: true + register: p12_backup_5 + + - name: "({{ select_crypto_backend }}) Generate 'empty' PKCS#12 file" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_empty.p12' + friendly_name: abracadabra + other_certificates: + - '{{ remote_tmp_dir }}/ansible2.crt' + - '{{ remote_tmp_dir }}/ansible3.crt' + state: present + register: p12_empty + + + - name: "({{ select_crypto_backend }}) Generate 'empty' PKCS#12 file (idempotent)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_empty.p12' + friendly_name: abracadabra + other_certificates: + - '{{ remote_tmp_dir }}/ansible3.crt' + - '{{ remote_tmp_dir }}/ansible2.crt' + state: present + register: p12_empty_idem + + - name: "({{ select_crypto_backend }}) Generate 'empty' PKCS#12 file (idempotent, concatenated other certificates)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_empty.p12' + friendly_name: abracadabra + other_certificates: + - '{{ remote_tmp_dir }}/ansible23.crt' + other_certificates_parse_all: true + state: present + register: p12_empty_concat_idem + + - name: "({{ select_crypto_backend }}) Generate 'empty' PKCS#12 file (parse)" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + src: '{{ remote_tmp_dir }}/ansible_empty.p12' + path: '{{ remote_tmp_dir }}/ansible_empty.pem' + action: parse + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file passphrase and compatibility encryption" + openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' + path: '{{ remote_tmp_dir }}/ansible_compatibility2022.p12' + friendly_name: compat_fn + encryption_level: compatibility2022 + iter_size: 3210 + passphrase: magicpassword + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + other_certificates: + - '{{ remote_tmp_dir }}/ansible2.crt' + - '{{ remote_tmp_dir }}/ansible3.crt' + state: present + register: p12_compatibility2022 + when: + - select_crypto_backend == 'cryptography' + - cryptography_version.stdout is version('38.0.0', '>=') + + - import_tasks: ../tests/validate.yml + + always: + - name: "({{ select_crypto_backend }}) Delete PKCS#12 file" + openssl_pkcs12: + state: absent + path: '{{ remote_tmp_dir }}/{{ item }}.p12' + loop: + - ansible + - ansible_no_pkey + - ansible_multi_certs + - ansible_pw1 + - ansible_pw2 + - ansible_pw3 + - ansible_empty + - ansible_compatibility2022 diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/tasks/main.yml new file mode 100644 index 000000000..7116c8674 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/tasks/main.yml @@ -0,0 +1,82 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- block: + - name: Generate private keys + openssl_privatekey: + path: '{{ remote_tmp_dir }}/ansible_pkey{{ item }}.pem' + size: '{{ default_rsa_key_size_certifiates }}' + loop: "{{ range(1, 4) | list }}" + + - name: Generate privatekey with password + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + size: '{{ default_rsa_key_size }}' + + - name: Generate CSRs + openssl_csr: + path: '{{ remote_tmp_dir }}/ansible{{ item }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey{{ item }}.pem' + commonName: www{{ item }}.ansible.com + loop: "{{ range(1, 4) | list }}" + + - name: Generate certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/ansible{{ item }}.crt' + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey{{ item }}.pem' + csr_path: '{{ remote_tmp_dir }}/ansible{{ item }}.csr' + provider: selfsigned + loop: "{{ range(1, 4) | list }}" + + - name: Read files + slurp: + src: '{{ item }}' + loop: + - "{{ remote_tmp_dir ~ '/ansible2.crt' }}" + - "{{ remote_tmp_dir ~ '/ansible3.crt' }}" + register: slurp + + - name: Generate concatenated PEM file + copy: + dest: '{{ remote_tmp_dir }}/ansible23.crt' + content: '{{ slurp.results[0].content | b64decode }}{{ slurp.results[1].content | b64decode }}' + + - name: Generate PKCS#12 file with backend autodetection + openssl_pkcs12: + path: '{{ remote_tmp_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ remote_tmp_dir }}/ansible_pkey1.pem' + certificate_path: '{{ remote_tmp_dir }}/ansible1.crt' + state: present + + - name: Delete result + file: + path: '{{ remote_tmp_dir }}/ansible.p12' + state: absent + + - block: + - name: Running tests with pyOpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: pyopenssl + + when: (pyopenssl_version.stdout | default('0.0')) is version('0.15', '>=') + + - block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('3.0', '>=') + + when: (pyopenssl_version.stdout | default('0.0')) is version('0.15', '>=') or cryptography_version.stdout is version('3.0', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/tests/validate.yml new file mode 100644 index 000000000..dc1b89c59 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_pkcs12/tests/validate.yml @@ -0,0 +1,112 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: '({{ select_crypto_backend }}) Validate PKCS#12' + command: "{{ openssl_binary }} pkcs12 -info -in {{ remote_tmp_dir }}/ansible.p12 -nodes -passin pass:''" + register: p12 + +- name: '({{ select_crypto_backend }}) Validate PKCS#12 with no private key' + command: "{{ openssl_binary }} pkcs12 -info -in {{ remote_tmp_dir }}/ansible_no_pkey.p12 -nodes -passin pass:''" + register: p12_validate_no_pkey + +- name: '({{ select_crypto_backend }}) Validate PKCS#12 with multiple certs' + shell: "{{ openssl_binary }} pkcs12 -info -in {{ remote_tmp_dir }}/ansible_multi_certs.p12 -nodes -passin pass:'hunter3' | grep subject" + register: p12_validate_multi_certs + +- name: '({{ select_crypto_backend }}) Validate PKCS#12 (assert)' + assert: + that: + - p12_standard_check is changed + - p12_standard is changed + - p12.stdout_lines[2].split(':')[-1].strip() == 'abracadabra' + - p12_standard.mode == '0400' + - p12_no_pkey is changed + - p12_validate_no_pkey.stdout_lines[-1] == '-----END CERTIFICATE-----' + - p12_force is changed + - p12_force_and_mode.mode == '0644' and p12_force_and_mode.changed + - p12_dumped is changed + - p12_standard_idempotency is not changed + - p12_standard_idempotency_check is not changed + - p12_standard_idempotency_no_certs is not changed + - p12_standard_idempotency_2 is not changed + - p12_multiple_certs_idempotency is not changed + - p12_dumped_idempotency is not changed + - p12_dumped_check_mode is not changed + - "'www1.' in p12_validate_multi_certs.stdout" + - "'www2.' in p12_validate_multi_certs.stdout" + - "'www3.' in p12_validate_multi_certs.stdout" + +- name: '({{ select_crypto_backend }}) Check passphrase on private key' + assert: + that: + - passphrase_error_1 is failed + - "'assphrase' in passphrase_error_1.msg or 'assword' in passphrase_error_1.msg" + - passphrase_error_2 is failed + - "'assphrase' in passphrase_error_2.msg or 'assword' in passphrase_error_2.msg or 'serializ' in passphrase_error_2.msg" + - passphrase_error_3 is failed + - "'assphrase' in passphrase_error_3.msg or 'assword' in passphrase_error_3.msg or 'serializ' in passphrase_error_3.msg" + +- name: '({{ select_crypto_backend }}) Verify that broken PKCS#12 will be regenerated' + assert: + that: + - output_broken is changed + +- name: '({{ select_crypto_backend }}) Check backup' + assert: + that: + - p12_backup_1 is changed + - p12_backup_1.backup_file is undefined + - p12_backup_2 is not changed + - p12_backup_2.backup_file is undefined + - p12_backup_3 is changed + - p12_backup_3.backup_file is string + - p12_backup_4 is changed + - p12_backup_4.backup_file is string + - p12_backup_5 is not changed + - p12_backup_5.backup_file is undefined + - p12_backup_4.pkcs12 is none + +- name: '({{ select_crypto_backend }}) Read files' + slurp: + src: '{{ item }}' + loop: + - "{{ remote_tmp_dir ~ '/ansible_empty.pem' }}" + - "{{ remote_tmp_dir ~ '/ansible2.crt' }}" + - "{{ remote_tmp_dir ~ '/ansible3.crt' }}" + register: slurp + +- name: '({{ select_crypto_backend }}) Load "empty" file' + set_fact: + empty_contents: "{{ slurp.results[0].content | b64decode }}" + empty_expected_pyopenssl: "{{ (slurp.results[2].content | b64decode) ~ (slurp.results[1].content | b64decode) }}" + empty_expected_cryptography: "{{ (slurp.results[1].content | b64decode) ~ (slurp.results[2].content | b64decode) }}" + +- name: '({{ select_crypto_backend }}) Check "empty" file' + assert: + that: + - p12_empty is changed + - p12_empty_idem is not changed + - p12_empty_concat_idem is not changed + - (empty_contents == empty_expected_cryptography) or (empty_contents == empty_expected_pyopenssl and select_crypto_backend == 'pyopenssl') + +- name: '({{ select_crypto_backend }}) PKCS#12 with compatibility2022 settings' + when: + - select_crypto_backend == 'cryptography' + - cryptography_version.stdout is version('38.0.0', '>=') + block: + - name: '({{ select_crypto_backend }}) Validate PKCS#12 with compatibility2022 settings' + shell: "{{ openssl_binary }} pkcs12 -info -in {{ remote_tmp_dir }}/ansible_compatibility2022.p12 -nodes -passin pass:'magicpassword'" + register: p12_validate_compatibility2022 + + - name: '({{ select_crypto_backend }}) Check PKCS#12 with compatibility2022 settings' + assert: + that: + - p12_compatibility2022 is changed + - >- + 'PKCS7 Encrypted data: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 3210' in p12_validate_compatibility2022.stderr_lines + - >- + 'Shrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 3210' in p12_validate_compatibility2022.stderr_lines + - >- + 'friendlyName: compat_fn' in p12_validate_compatibility2022.stdout diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/tasks/impl.yml new file mode 100644 index 000000000..f12d23ede --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/tasks/impl.yml @@ -0,0 +1,879 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "({{ select_crypto_backend }}) Generate privatekey1 - standard (check mode)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + check_mode: true + register: privatekey1_check + +- name: "({{ select_crypto_backend }}) Generate privatekey1 - standard" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: privatekey1 + +- name: "({{ select_crypto_backend }}) Generate privatekey1 - standard (idempotence, check mode)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + check_mode: true + register: privatekey1_idempotence_check + +- name: "({{ select_crypto_backend }}) Generate privatekey1 - standard (idempotence)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: privatekey1_idempotence + +- name: "({{ select_crypto_backend }}) Generate privatekey2 - size 2048" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey2.pem' + size: 2048 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "({{ select_crypto_backend }}) Generate privatekey3 - type DSA" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey3.pem' + type: DSA + size: 3072 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "({{ select_crypto_backend }}) Generate privatekey4 - standard" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey4.pem' + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "({{ select_crypto_backend }}) Delete privatekey4 - standard" + openssl_privatekey: + state: absent + path: '{{ remote_tmp_dir }}/privatekey4.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: privatekey4_delete + +- name: "({{ select_crypto_backend }}) Delete privatekey4 - standard (idempotence)" + openssl_privatekey: + state: absent + path: '{{ remote_tmp_dir }}/privatekey4.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey4_delete_idempotence + +- name: "({{ select_crypto_backend }}) Generate privatekey5 - standard - with passphrase" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey5.pem' + passphrase: ansible + cipher: auto + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "({{ select_crypto_backend }}) Generate privatekey5 - standard - idempotence" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey5.pem' + passphrase: ansible + cipher: auto + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey5_idempotence + +- name: "({{ select_crypto_backend }}) Generate privatekey6 - standard - with non-ASCII passphrase" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey6.pem' + passphrase: ànsïblé + cipher: auto + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + +- set_fact: + ecc_types: + - curve: secp384r1 + openssl_name: secp384r1 + min_cryptography_version: "0.5" + - curve: secp521r1 + openssl_name: secp521r1 + min_cryptography_version: "0.5" + - curve: secp224r1 + openssl_name: secp224r1 + min_cryptography_version: "0.5" + - curve: secp192r1 + openssl_name: prime192v1 + min_cryptography_version: "0.5" + - curve: secp256r1 + openssl_name: secp256r1 + min_cryptography_version: "0.5" + - curve: secp256k1 + openssl_name: secp256k1 + min_cryptography_version: "0.9" + - curve: brainpoolP256r1 + openssl_name: brainpoolP256r1 + min_cryptography_version: "2.2" + - curve: brainpoolP384r1 + openssl_name: brainpoolP384r1 + min_cryptography_version: "2.2" + - curve: brainpoolP512r1 + openssl_name: brainpoolP512r1 + min_cryptography_version: "2.2" + - curve: sect571k1 + openssl_name: sect571k1 + min_cryptography_version: "0.5" + - curve: sect409k1 + openssl_name: sect409k1 + min_cryptography_version: "0.5" + - curve: sect283k1 + openssl_name: sect283k1 + min_cryptography_version: "0.5" + - curve: sect233k1 + openssl_name: sect233k1 + min_cryptography_version: "0.5" + - curve: sect163k1 + openssl_name: sect163k1 + min_cryptography_version: "0.5" + - curve: sect571r1 + openssl_name: sect571r1 + min_cryptography_version: "0.5" + - curve: sect409r1 + openssl_name: sect409r1 + min_cryptography_version: "0.5" + - curve: sect283r1 + openssl_name: sect283r1 + min_cryptography_version: "0.5" + - curve: sect233r1 + openssl_name: sect233r1 + min_cryptography_version: "0.5" + - curve: sect163r2 + openssl_name: sect163r2 + min_cryptography_version: "0.5" + +- name: "({{ select_crypto_backend }}) Test ECC key generation" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey-{{ item.curve }}.pem' + type: ECC + curve: "{{ item.curve }}" + select_crypto_backend: '{{ select_crypto_backend }}' + when: | + cryptography_version.stdout is version(item.min_cryptography_version, '>=') and + item.openssl_name in openssl_ecc_list + loop: "{{ ecc_types }}" + loop_control: + label: "{{ item.curve }}" + register: privatekey_ecc_generate + +- name: "({{ select_crypto_backend }}) Test ECC key generation (idempotency)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey-{{ item.curve }}.pem' + type: ECC + curve: "{{ item.curve }}" + select_crypto_backend: '{{ select_crypto_backend }}' + when: | + cryptography_version.stdout is version(item.min_cryptography_version, '>=') and + item.openssl_name in openssl_ecc_list + loop: "{{ ecc_types }}" + loop_control: + label: "{{ item.curve }}" + register: privatekey_ecc_idempotency + +- block: + - name: "({{ select_crypto_backend }}) Test other type generation" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey-{{ item.type }}.pem' + type: "{{ item.type }}" + select_crypto_backend: '{{ select_crypto_backend }}' + when: cryptography_version.stdout is version(item.min_version, '>=') + loop: "{{ types }}" + loop_control: + label: "{{ item.type }}" + ignore_errors: true + register: privatekey_t1_generate + + - name: "({{ select_crypto_backend }}) Test other type generation (idempotency)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey-{{ item.type }}.pem' + type: "{{ item.type }}" + select_crypto_backend: '{{ select_crypto_backend }}' + when: cryptography_version.stdout is version(item.min_version, '>=') + loop: "{{ types }}" + loop_control: + label: "{{ item.type }}" + ignore_errors: true + register: privatekey_t1_idempotency + + when: select_crypto_backend == 'cryptography' + vars: + types: + - type: X25519 + min_version: '2.5' + - type: Ed25519 + min_version: '2.6' + - type: Ed448 + min_version: '2.6' + - type: X448 + min_version: '2.6' + +- name: "({{ select_crypto_backend }}) Generate privatekey with passphrase" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + backup: true + register: passphrase_1 + +- name: "({{ select_crypto_backend }}) Generate privatekey with passphrase (idempotent)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + backup: true + register: passphrase_2 + +- name: "({{ select_crypto_backend }}) Regenerate privatekey without passphrase" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + backup: true + register: passphrase_3 + +- name: "({{ select_crypto_backend }}) Regenerate privatekey without passphrase (idempotent)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + backup: true + register: passphrase_4 + +- name: "({{ select_crypto_backend }}) Regenerate privatekey with passphrase" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + backup: true + register: passphrase_5 + +- name: "({{ select_crypto_backend }}) Create broken key" + copy: + dest: "{{ remote_tmp_dir }}/broken" + content: "broken" +- name: "({{ select_crypto_backend }}) Regenerate broken key" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/broken.pem' + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: output_broken + +- name: "({{ select_crypto_backend }}) Remove module" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + backup: true + state: absent + register: remove_1 + +- name: "({{ select_crypto_backend }}) Remove module (idempotent)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + backup: true + state: absent + register: remove_2 + +- name: "({{ select_crypto_backend }}) Generate privatekey_mode (mode 0400)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_mode.pem' + mode: '0400' + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_mode_1 + +- name: "({{ select_crypto_backend }}) Stat for privatekey_mode" + stat: + path: '{{ remote_tmp_dir }}/privatekey_mode.pem' + register: privatekey_mode_1_stat + +- name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/privatekey_mode.pem' + register: privatekey_mode_1_fileinfo + +- name: "({{ select_crypto_backend }}) Generate privatekey_mode (mode 0400, idempotency)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_mode.pem' + mode: '0400' + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_mode_2 + +- name: "({{ select_crypto_backend }}) Generate privatekey_mode (mode 0400, force)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_mode.pem' + mode: '0400' + force: true + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_mode_3 + +- name: "({{ select_crypto_backend }}) Stat for privatekey_mode" + stat: + path: '{{ remote_tmp_dir }}/privatekey_mode.pem' + register: privatekey_mode_3_stat + +- name: "({{ select_crypto_backend }}) Make sure that file changed" + community.internal_test_tools.files_diff: + state: '{{ privatekey_mode_1_fileinfo }}' + register: privatekey_mode_3_file_change + +- block: + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_1 - auto format" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_1.pem' + format: auto + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_1 + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_1 - auto format (idempotent)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_1.pem' + format: auto + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_2 + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_1 - PKCS1 format" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_1.pem' + format: pkcs1 + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_3 + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_1 - PKCS8 format" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_1.pem' + format: pkcs8 + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_4 + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_1 - PKCS8 format (idempotent)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_1.pem' + format: pkcs8 + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_5 + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_1 - auto format (ignore)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_1.pem' + format: auto_ignore + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_6 + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_1 - auto format (no ignore)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_1.pem' + format: auto + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_7 + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_1 - raw format (fail)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_1.pem' + format: raw + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: privatekey_fmt_1_step_8 + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_1 - PKCS8 format (convert)" + openssl_privatekey_info: + path: '{{ remote_tmp_dir }}/privatekey_fmt_1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_9_before + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_1 - PKCS8 format (convert)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_1.pem' + format: pkcs8 + format_mismatch: convert + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_9 + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_1 - PKCS8 format (convert)" + openssl_privatekey_info: + path: '{{ remote_tmp_dir }}/privatekey_fmt_1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey_fmt_1_step_9_after + + when: 'select_crypto_backend == "cryptography"' + +- block: + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_2 - PKCS8 format" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_2.pem' + type: X448 + format: pkcs8 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: privatekey_fmt_2_step_1 + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_2 - PKCS8 format (idempotent)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_2.pem' + type: X448 + format: pkcs8 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: privatekey_fmt_2_step_2 + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_2 - raw format" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_2.pem' + type: X448 + format: raw + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + ignore_errors: true + register: privatekey_fmt_2_step_3 + + - name: "({{ select_crypto_backend }}) Read privatekey_fmt_2.pem" + slurp: + src: "{{ remote_tmp_dir }}/privatekey_fmt_2.pem" + ignore_errors: true + register: content + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_2 - verify that returned content is base64 encoded" + assert: + that: + - privatekey_fmt_2_step_3.privatekey == content.content + when: privatekey_fmt_2_step_1 is not failed + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_2 - raw format (idempotent)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_2.pem' + type: X448 + format: raw + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + ignore_errors: true + register: privatekey_fmt_2_step_4 + + - name: "({{ select_crypto_backend }}) Read privatekey_fmt_2.pem" + slurp: + src: "{{ remote_tmp_dir }}/privatekey_fmt_2.pem" + ignore_errors: true + register: content + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_2 - verify that returned content is base64 encoded" + assert: + that: + - privatekey_fmt_2_step_4.privatekey == content.content + when: privatekey_fmt_2_step_1 is not failed + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_2 - auto format (ignore)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_2.pem' + type: X448 + format: auto_ignore + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + ignore_errors: true + register: privatekey_fmt_2_step_5 + + - name: "({{ select_crypto_backend }}) Read privatekey_fmt_2.pem" + slurp: + src: "{{ remote_tmp_dir }}/privatekey_fmt_2.pem" + ignore_errors: true + register: content + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_2 - verify that returned content is base64 encoded" + assert: + that: + - privatekey_fmt_2_step_5.privatekey == content.content + when: privatekey_fmt_2_step_1 is not failed + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_2 - auto format (no ignore)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_fmt_2.pem' + type: X448 + format: auto + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + ignore_errors: true + register: privatekey_fmt_2_step_6 + + - name: "({{ select_crypto_backend }}) Read private key" + slurp: + src: '{{ remote_tmp_dir }}/privatekey_fmt_2.pem' + register: slurp + when: privatekey_fmt_2_step_1 is not failed + + - name: "({{ select_crypto_backend }}) Generate privatekey_fmt_2 - verify that returned content is not base64 encoded" + assert: + that: + - privatekey_fmt_2_step_6.privatekey == (slurp.content | b64decode) + when: privatekey_fmt_2_step_1 is not failed + + when: 'select_crypto_backend == "cryptography" and cryptography_version.stdout is version("2.6", ">=")' + + + +# Test regenerate option + +- name: "({{ select_crypto_backend }}) Regenerate - setup simple keys" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" +- name: "({{ select_crypto_backend }}) Regenerate - setup password protected keys" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-b-{{ item }}.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + passphrase: hunter2 + cipher: auto + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" +- name: "({{ select_crypto_backend }}) Regenerate - setup broken keys" + copy: + dest: '{{ remote_tmp_dir }}/regenerate-c-{{ item }}.pem' + content: 'broken key' + mode: '0700' + loop: "{{ regenerate_values }}" + +- name: "({{ select_crypto_backend }}) Regenerate - modify broken keys (check mode)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-c-{{ item }}.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[0].msg or 'Cannot load raw key' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[1].msg or 'Cannot load raw key' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[2].msg or 'Cannot load raw key' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ select_crypto_backend }}) Regenerate - modify broken keys" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-c-{{ item }}.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[0].msg or 'Cannot load raw key' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[1].msg or 'Cannot load raw key' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[2].msg or 'Cannot load raw key' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ select_crypto_backend }}) Regenerate - modify password protected keys (check mode)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-b-{{ item }}.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ select_crypto_backend }}) Regenerate - modify password protected keys" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-b-{{ item }}.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[0].msg" + - result.results[1] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[1].msg" + - result.results[2] is failed + - "'Unable to read the key. The key is protected with a another passphrase / no passphrase or broken. Will not proceed.' in result.results[2].msg" + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ select_crypto_backend }}) Regenerate - not modify regular keys (check mode)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result.results[0] is not changed + - result.results[1] is not changed + - result.results[2] is not changed + - result.results[3] is not changed + - result.results[4] is changed + +- name: "({{ select_crypto_backend }}) Regenerate - not modify regular keys" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + register: result +- assert: + that: + - result.results[0] is not changed + - result.results[1] is not changed + - result.results[2] is not changed + - result.results[3] is not changed + - result.results[4] is changed + +- name: "({{ select_crypto_backend }}) Regenerate - adjust key size (check mode)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: '{{ default_rsa_key_size + 20 }}' + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ select_crypto_backend }}) Regenerate - adjust key size" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + type: RSA + size: '{{ default_rsa_key_size + 20 }}' + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ select_crypto_backend }}) Regenerate - redistribute keys" + copy: + src: '{{ remote_tmp_dir }}/regenerate-a-always.pem' + dest: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + remote_src: true + loop: "{{ regenerate_values }}" + when: "item != 'always'" + +- name: "({{ select_crypto_backend }}) Regenerate - adjust key type (check mode)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: '{{ default_rsa_key_size }}' + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- name: "({{ select_crypto_backend }}) Regenerate - adjust key type" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: '{{ default_rsa_key_size }}' + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result +- assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + +- block: + - name: "({{ select_crypto_backend }}) Regenerate - redistribute keys" + copy: + src: '{{ remote_tmp_dir }}/regenerate-a-always.pem' + dest: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + remote_src: true + loop: "{{ regenerate_values }}" + when: "item != 'always'" + + - name: "({{ select_crypto_backend }}) Regenerate - format mismatch (check mode)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: '{{ default_rsa_key_size }}' + format: pkcs8 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result + - assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong format. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + + - name: "({{ select_crypto_backend }}) Regenerate - format mismatch" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: '{{ default_rsa_key_size }}' + format: pkcs8 + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + ignore_errors: true + register: result + - assert: + that: + - result.results[0] is success and result.results[0] is not changed + - result.results[1] is failed + - "'Key has wrong format. Will not proceed.' in result.results[1].msg" + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + + - name: "({{ select_crypto_backend }}) Regenerate - redistribute keys" + copy: + src: '{{ remote_tmp_dir }}/regenerate-a-always.pem' + dest: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + remote_src: true + loop: "{{ regenerate_values }}" + when: "item != 'always'" + + - name: "({{ select_crypto_backend }}) Regenerate - convert format (check mode)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: '{{ default_rsa_key_size }}' + format: pkcs1 + format_mismatch: convert + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + loop: "{{ regenerate_values }}" + register: result + - assert: + that: + - result.results[0] is changed + - result.results[1] is changed + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + + - name: "({{ select_crypto_backend }}) Regenerate - convert format" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/regenerate-a-{{ item }}.pem' + type: DSA + size: '{{ default_rsa_key_size }}' + format: pkcs1 + format_mismatch: convert + regenerate: '{{ item }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: "{{ regenerate_values }}" + register: result + - assert: + that: + - result.results[0] is changed + - result.results[1] is changed + - result.results[2] is changed + - result.results[3] is changed + - result.results[4] is changed + # for all values but 'always', the key should have not been regenerated. + # verify this by comparing fingerprints: + - result.results[0].fingerprint == result.results[1].fingerprint + - result.results[0].fingerprint == result.results[2].fingerprint + - result.results[0].fingerprint == result.results[3].fingerprint + - result.results[0].fingerprint != result.results[4].fingerprint + when: 'select_crypto_backend == "cryptography" and cryptography_version.stdout is version("2.6", ">=")' diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/tasks/main.yml new file mode 100644 index 000000000..9154bf9e5 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/tasks/main.yml @@ -0,0 +1,53 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Find out which elliptic curves are supported by installed OpenSSL + command: "{{ openssl_binary }} ecparam -list_curves" + register: openssl_ecc + +- name: Compile list of elliptic curves supported by OpenSSL + set_fact: + openssl_ecc_list: | + {{ + openssl_ecc.stdout_lines + | map('regex_search', '^ *([a-zA-Z0-9_-]+) *: .*$') + | select() + | map('regex_replace', '^ *([a-zA-Z0-9_-]+) *: .*$', '\1') + | list + }} + when: ansible_distribution != 'CentOS' or ansible_distribution_major_version != '6' + # CentOS comes with a very old jinja2 which does not include the map() filter... +- name: Compile list of elliptic curves supported by OpenSSL (CentOS 6) + set_fact: + openssl_ecc_list: + - secp384r1 + - secp521r1 + - prime256v1 + when: ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6' + +- name: List of elliptic curves supported by OpenSSL + debug: var=openssl_ecc_list + +- name: Run module with backend autodetection + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_backend_selection.pem' + size: '{{ default_rsa_key_size }}' + +- 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('0.5', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/tests/validate.yml new file mode 100644 index 000000000..8f134dddf --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/tests/validate.yml @@ -0,0 +1,227 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- set_fact: + system_potentially_has_no_algorithm_support: "{{ ansible_os_family == 'FreeBSD' }}" + +- name: "({{ select_crypto_backend }}) Read private key" + slurp: + src: '{{ remote_tmp_dir }}/privatekey1.pem' + register: slurp + +- name: "({{ select_crypto_backend }}) Validate privatekey1 idempotency and content returned" + assert: + that: + - privatekey1_check is changed + - privatekey1 is changed + - privatekey1_idempotence_check is not changed + - privatekey1_idempotence is not changed + - privatekey1.privatekey == (slurp.content | b64decode) + - privatekey1.privatekey == privatekey1_idempotence.privatekey + + +- name: "({{ select_crypto_backend }}) Validate privatekey1 (test - RSA key with size 4096 bits)" + shell: "{{ openssl_binary }} rsa -noout -text -in {{ remote_tmp_dir }}/privatekey1.pem | grep Private | sed 's/\\(RSA *\\)*Private-Key: (\\(.*\\) bit.*)/\\2/'" + register: privatekey1 + +- name: "({{ select_crypto_backend }}) Validate privatekey1 (assert - RSA key with size 4096 bits)" + assert: + that: + - privatekey1.stdout == '4096' + + +- name: "({{ select_crypto_backend }}) Validate privatekey2 (test - RSA key with size 2048 bits)" + shell: "{{ openssl_binary }} rsa -noout -text -in {{ remote_tmp_dir }}/privatekey2.pem | grep Private | sed 's/\\(RSA *\\)*Private-Key: (\\(.*\\) bit.*)/\\2/'" + register: privatekey2 + +- name: "({{ select_crypto_backend }}) Validate privatekey2 (assert - RSA key with size 2048 bits)" + assert: + that: + - privatekey2.stdout == '2048' + + +- name: "({{ select_crypto_backend }}) Validate privatekey3 (test - DSA key with size 3072 bits)" + shell: "{{ openssl_binary }} dsa -noout -text -in {{ remote_tmp_dir }}/privatekey3.pem | grep Private | sed 's/\\(RSA *\\)*Private-Key: (\\(.*\\) bit.*)/\\2/'" + register: privatekey3 + +- name: Validate privatekey3 (assert - DSA key with size 3072 bits) + assert: + that: + - privatekey3.stdout == '3072' + + +- name: "({{ select_crypto_backend }}) Validate privatekey4 (test - Ensure key has been removed)" + stat: + path: '{{ remote_tmp_dir }}/privatekey4.pem' + register: privatekey4 + +- name: "({{ select_crypto_backend }}) Validate privatekey4 (assert - Ensure key has been removed)" + assert: + that: + - privatekey4.stat.exists == False + +- name: "({{ select_crypto_backend }}) Validate privatekey4 removal behavior" + assert: + that: + - privatekey4_delete is changed + - privatekey4_delete.privatekey is none + - privatekey4_delete_idempotence is not changed + + +- name: "({{ select_crypto_backend }}) Validate privatekey5 (test - Passphrase protected key + idempotence)" + shell: "{{ openssl_binary }} rsa -noout -text -in {{ remote_tmp_dir }}/privatekey5.pem -passin pass:ansible | grep Private | sed 's/\\(RSA *\\)*Private-Key: (\\(.*\\) bit.*)/\\2/'" + register: privatekey5 + # Current version of OS/X that runs in the CI (10.11) does not have an up to date version of the OpenSSL library + # leading to this test to fail when run in the CI. However, this test has been run for 10.12 and has returned succesfully. + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: "({{ select_crypto_backend }}) Validate privatekey5 (assert - Passphrase protected key + idempotence)" + assert: + that: + - privatekey5.stdout == '{{ default_rsa_key_size }}' + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: "({{ select_crypto_backend }}) Validate privatekey5 idempotence (assert - Passphrase protected key + idempotence)" + assert: + that: + - privatekey5_idempotence is not changed + + +- name: "({{ select_crypto_backend }}) Validate privatekey6 (test - Passphrase protected key with non ascii character)" + shell: "{{ openssl_binary }} rsa -noout -text -in {{ remote_tmp_dir }}/privatekey6.pem -passin pass:ànsïblé | grep Private | sed 's/\\(RSA *\\)*Private-Key: (\\(.*\\) bit.*)/\\2/'" + register: privatekey6 + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: "({{ select_crypto_backend }}) Validate privatekey6 (assert - Passphrase protected key with non ascii character)" + assert: + that: + - privatekey6.stdout == '{{ default_rsa_key_size }}' + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: "({{ select_crypto_backend }}) Validate ECC generation (dump with OpenSSL)" + shell: "{{ openssl_binary }} ec -in {{ remote_tmp_dir }}/privatekey-{{ item.item.curve }}.pem -noout -text | grep 'ASN1 OID: ' | sed 's/ASN1 OID: \\([^ ]*\\)/\\1/'" + loop: "{{ privatekey_ecc_generate.results }}" + register: privatekey_ecc_dump + when: openssl_version.stdout is version('0.9.8zh', '>=') and 'skip_reason' not in item + loop_control: + label: "{{ item.item.curve }}" + +- name: "({{ select_crypto_backend }}) Validate ECC generation" + assert: + that: + - item is changed + loop: "{{ privatekey_ecc_generate.results }}" + when: "'skip_reason' not in item" + loop_control: + label: "{{ item.item.curve }}" + +- name: "({{ select_crypto_backend }}) Validate ECC generation (curve type)" + assert: + that: + - "'skip_reason' in item or item.item.item.openssl_name == item.stdout" + loop: "{{ privatekey_ecc_dump.results }}" + when: "'skip_reason' not in item" + loop_control: + label: "{{ item.item.item }} - {{ item.stdout if 'stdout' in item else '<unsupported>' }}" + +- name: "({{ select_crypto_backend }}) Validate ECC generation idempotency" + assert: + that: + - item is not changed + loop: "{{ privatekey_ecc_idempotency.results }}" + when: "'skip_reason' not in item" + loop_control: + label: "{{ item.item.curve }}" + +- name: "({{ select_crypto_backend }}) Validate other type generation (just check changed)" + assert: + that: + - (item is succeeded and item is changed) or + (item is failed and 'Cryptography backend does not support the algorithm required for ' in item.msg and system_potentially_has_no_algorithm_support) + loop: "{{ privatekey_t1_generate.results }}" + when: "'skip_reason' not in item" + loop_control: + label: "{{ item.item.type }}" + +- name: "({{ select_crypto_backend }}) Validate other type generation idempotency" + assert: + that: + - (item is succeeded and item is not changed) or + (item is failed and 'Cryptography backend does not support the algorithm required for ' in item.msg and system_potentially_has_no_algorithm_support) + loop: "{{ privatekey_t1_idempotency.results }}" + when: "'skip_reason' not in item" + loop_control: + label: "{{ item.item.type }}" + +- name: "({{ select_crypto_backend }}) Validate passphrase changing" + assert: + that: + - passphrase_1 is changed + - passphrase_2 is not changed + - passphrase_3 is changed + - passphrase_4 is not changed + - passphrase_5 is changed + - passphrase_1.backup_file is undefined + - passphrase_2.backup_file is undefined + - passphrase_3.backup_file is string + - passphrase_4.backup_file is undefined + - passphrase_5.backup_file is string + +- name: "({{ select_crypto_backend }}) Verify that broken key will be regenerated" + assert: + that: + - output_broken is changed + +- name: "({{ select_crypto_backend }}) Validate remove" + assert: + that: + - remove_1 is changed + - remove_2 is not changed + - remove_1.backup_file is string + - remove_2.backup_file is undefined + +- name: "({{ select_crypto_backend }}) Validate mode" + assert: + that: + - privatekey_mode_1 is changed + - privatekey_mode_1_stat.stat.mode == '0400' + - privatekey_mode_2 is not changed + - privatekey_mode_3 is changed + - privatekey_mode_3_stat.stat.mode == '0400' + - privatekey_mode_3_file_change is changed + +- name: "({{ select_crypto_backend }}) Validate format 1" + assert: + that: + - privatekey_fmt_1_step_1 is changed + - privatekey_fmt_1_step_2 is not changed + - privatekey_fmt_1_step_3 is not changed + - privatekey_fmt_1_step_4 is changed + - privatekey_fmt_1_step_5 is not changed + - privatekey_fmt_1_step_6 is not changed + - privatekey_fmt_1_step_7 is changed + - privatekey_fmt_1_step_8 is failed + - privatekey_fmt_1_step_9 is changed + - privatekey_fmt_1_step_9_before.public_key == privatekey_fmt_1_step_9_after.public_key + when: 'select_crypto_backend == "cryptography"' + +- name: "({{ select_crypto_backend }}) Validate format 2 (failed)" + assert: + that: + - system_potentially_has_no_algorithm_support + - privatekey_fmt_2_step_1 is failed + - "'Cryptography backend does not support the algorithm required for ' in privatekey_fmt_2_step_1.msg" + when: 'select_crypto_backend == "cryptography" and cryptography_version.stdout is version("2.6", ">=") and privatekey_fmt_2_step_1 is failed' + +- name: "({{ select_crypto_backend }}) Validate format 2" + assert: + that: + - privatekey_fmt_2_step_1 is succeeded and privatekey_fmt_2_step_1 is changed + - privatekey_fmt_2_step_2 is succeeded and privatekey_fmt_2_step_2 is not changed + - privatekey_fmt_2_step_3 is succeeded and privatekey_fmt_2_step_3 is changed + - privatekey_fmt_2_step_4 is succeeded and privatekey_fmt_2_step_4 is not changed + - privatekey_fmt_2_step_5 is succeeded and privatekey_fmt_2_step_5 is not changed + - privatekey_fmt_2_step_6 is succeeded and privatekey_fmt_2_step_6 is changed + when: 'select_crypto_backend == "cryptography" and cryptography_version.stdout is version("2.6", ">=") and privatekey_fmt_2_step_1 is not failed' diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/vars/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/vars/main.yml new file mode 100644 index 000000000..4362bd2aa --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey/vars/main.yml @@ -0,0 +1,11 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +regenerate_values: + - never + - fail + - partial_idempotence + - full_idempotence + - always diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_convert/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_convert/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_convert/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_convert/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_convert/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_convert/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_convert/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_convert/tasks/impl.yml new file mode 100644 index 000000000..bdaf6ea7b --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_convert/tasks/impl.yml @@ -0,0 +1,390 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Convert (check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs8 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_check + check_mode: true + +- name: Convert + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs8 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert + +- assert: + that: + - convert_check is changed + - convert is changed + +- name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/output_1.pem' + register: convert_file_info_data + +- name: Convert (idempotent, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs8 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem_check + check_mode: true + +- name: Convert (idempotent) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs8 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_idem_check is not changed + - convert_idem is not changed + - convert_file_info is not changed + +- name: Convert (change format, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem_check + check_mode: true + +- name: Convert (change format) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_not_idem_check is changed + - convert_not_idem is changed + - convert_file_info is changed + +- name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/output_1.pem' + register: convert_file_info_data + +- name: Convert (idempotent, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem_check + check_mode: true + +- name: Convert (idempotent) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_idem_check is not changed + - convert_idem is not changed + - convert_file_info is not changed + +- name: Convert (change password, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter3 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem_check + check_mode: true + +- name: Convert (change password) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter3 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_not_idem_check is changed + - convert_not_idem is changed + - convert_file_info is changed + +- name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/output_1.pem' + register: convert_file_info_data + +- name: Convert (idempotent, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter3 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem_check + check_mode: true + +- name: Convert (idempotent) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter3 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_idem_check is not changed + - convert_idem is not changed + - convert_file_info is not changed + +- name: Convert (remove password, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem_check + check_mode: true + +- name: Convert (remove password) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_not_idem_check is changed + - convert_not_idem is changed + - convert_file_info is changed + +- name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/output_1.pem' + register: convert_file_info_data + +- name: Convert (idempotent, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem_check + check_mode: true + +- name: Convert (idempotent) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_idem_check is not changed + - convert_idem is not changed + - convert_file_info is not changed + +- when: supports_ed25519 | bool + block: + - name: Convert (change format to raw, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_ed25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_2.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem_check + check_mode: true + + - name: Convert (change format to raw) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_ed25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_2.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem + + - assert: + that: + - convert_not_idem_check is changed + - convert_not_idem is changed + + - name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/output_2.pem' + register: convert_file_info_data + + - name: Convert (idempotent, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_ed25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_2.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem_check + check_mode: true + + - name: Convert (idempotent) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_ed25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_2.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem + + - name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + + - assert: + that: + - convert_idem_check is not changed + - convert_idem is not changed + - convert_file_info is not changed + +- when: supports_x25519 | bool + block: + - name: Convert (change format to raw, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_x25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_3.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem_check + check_mode: true + + - name: Convert (change format to raw) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_x25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_3.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem + + - assert: + that: + - convert_not_idem_check is changed + - convert_not_idem is changed + + - name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/output_3.pem' + register: convert_file_info_data + + - name: Convert (idempotent, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_x25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_3.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem_check + check_mode: true + + - name: Convert (idempotent) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_x25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_3.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem + + - name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + + - assert: + that: + - convert_idem_check is not changed + - convert_idem is not changed + - convert_file_info is not changed diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_convert/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_convert/tasks/main.yml new file mode 100644 index 000000000..ea1dff8ac --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_convert/tasks/main.yml @@ -0,0 +1,65 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Determine capabilities + set_fact: + supports_x25519: '{{ cryptography_version.stdout is version("2.5", ">=") }}' + supports_ed25519: >- + {{ + cryptography_version.stdout is version("2.6", ">=") + and not ( + ansible_os_family == "FreeBSD" and + ansible_facts.distribution_version is version("12.1", ">=") and + ansible_facts.distribution_version is version("12.2", "<") + ) + }} + +- name: Create keys + openssl_privatekey: + size: '{{ item.size | default(omit) }}' + path: '{{ remote_tmp_dir }}/privatekey_{{ item.name }}.pem' + type: '{{ item.type | default(omit) }}' + curve: '{{ item.curve | default(omit) }}' + passphrase: '{{ item.passphrase | default(omit) }}' + cipher: '{{ "auto" if item.passphrase is defined else omit }}' + format: '{{ item.format }}' + when: item.condition | default(true) + loop: + - name: rsa_pass1 + format: pkcs1 + type: RSA + size: '{{ default_rsa_key_size }}' + passphrase: secret + - name: ed25519 + format: pkcs8 + type: Ed25519 + size: '{{ default_rsa_key_size }}' + condition: '{{ supports_ed25519 }}' + - name: x25519 + format: pkcs8 + type: X25519 + size: '{{ default_rsa_key_size }}' + condition: '{{ supports_x25519 }}' + +- name: Run module with backend autodetection + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_backend_selection.pem' + dest_passphrase: hunter2 + format: pkcs8 + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('1.2.3', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_info/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_info/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_info/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_info/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_info/meta/main.yml new file mode 100644 index 000000000..7c2b42405 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_info/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir + - prepare_jinja2_compat diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_info/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_info/tasks/impl.yml new file mode 100644 index 000000000..00c2320ec --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_info/tasks/impl.yml @@ -0,0 +1,154 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- debug: + msg: "Executing tests with backend {{ select_crypto_backend }}" + +- name: ({{select_crypto_backend}}) Get key 1 info + openssl_privatekey_info: + path: '{{ remote_tmp_dir }}/privatekey_1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that RSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + - "'private_data' not in result" + +- name: ({{select_crypto_backend}}) Read private key + slurp: + src: '{{ remote_tmp_dir }}/privatekey_1.pem' + register: slurp + +- name: ({{select_crypto_backend}}) Get key 1 info directly + openssl_privatekey_info: + content: '{{ slurp.content | b64decode }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_direct + +- name: ({{select_crypto_backend}}) Compare output of direct and loaded info + assert: + that: + - result == result_direct + +- name: ({{select_crypto_backend}}) Get key 2 info + openssl_privatekey_info: + path: '{{ remote_tmp_dir }}/privatekey_2.pem' + return_private_key_data: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that RSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "result.public_data.size == default_rsa_key_size" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + - "'private_data' in result" + - "result.public_data.modulus == result.private_data.p * result.private_data.q" + - "result.private_data.exponent > 5" + +- name: ({{select_crypto_backend}}) Get key 3 info (without passphrase) + openssl_privatekey_info: + path: '{{ remote_tmp_dir }}/privatekey_3.pem' + return_private_key_data: true + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: result + +- name: Check that loading passphrase protected key without passphrase failed + assert: + that: + - result is failed + # Check that return values are there + - result.can_load_key is defined + - result.can_parse_key is defined + # Check that return values are correct + - result.can_load_key + - not result.can_parse_key + # Check that additional data isn't there + - "'pulic_key' not in result" + - "'pulic_key_fingerprints' not in result" + - "'type' not in result" + - "'public_data' not in result" + - "'private_data' not in result" + +- name: ({{select_crypto_backend}}) Get key 3 info (with passphrase) + openssl_privatekey_info: + path: '{{ remote_tmp_dir }}/privatekey_3.pem' + passphrase: hunter2 + return_private_key_data: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that RSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + - "'private_data' in result" + - "result.public_data.modulus == result.private_data.p * result.private_data.q" + - "result.private_data.exponent > 5" + +- name: ({{select_crypto_backend}}) Get key 4 info + openssl_privatekey_info: + path: '{{ remote_tmp_dir }}/privatekey_4.pem' + return_private_key_data: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that ECC key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'ECC'" + - "'public_data' in result" + - "result.public_data.curve is string" + - "result.public_data.x != 0" + - "result.public_data.y != 0" + - "result.public_data.exponent_size == (521 if (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') else 256)" + - "'private_data' in result" + - "result.private_data.multiplier > 1024" + +- name: ({{select_crypto_backend}}) Get key 5 info + openssl_privatekey_info: + path: '{{ remote_tmp_dir }}/privatekey_5.pem' + return_private_key_data: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that DSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'DSA'" + - "'public_data' in result" + - "result.public_data.p > 2" + - "result.public_data.q > 2" + - "result.public_data.g >= 2" + - "result.public_data.y > 2" + - "'private_data' in result" + - "result.private_data.x > 2" diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_info/tasks/main.yml new file mode 100644 index 000000000..002608cd3 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_info/tasks/main.yml @@ -0,0 +1,47 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Generate privatekey 1 + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_1.pem' + +- name: Generate privatekey 2 (less bits) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_2.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + +- name: Generate privatekey 3 (with password) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_3.pem' + passphrase: hunter2 + cipher: auto + size: '{{ default_rsa_key_size }}' + select_crypto_backend: cryptography + +- name: Generate privatekey 4 (ECC) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_4.pem' + type: ECC + curve: "{{ (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') | ternary('secp521r1', 'secp256k1') }}" + # ^ cryptography on CentOS6 doesn't support secp256k1, so we use secp521r1 instead + select_crypto_backend: cryptography + +- name: Generate privatekey 5 (DSA) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_5.pem' + type: DSA + size: 1024 + +- name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + when: cryptography_version.stdout is version('1.2.3', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_pipe/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_pipe/aliases new file mode 100644 index 000000000..00bbb3ddd --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_pipe/aliases @@ -0,0 +1,8 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +context/controller +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_pipe/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_pipe/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_pipe/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_pipe/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_pipe/tasks/impl.yml new file mode 100644 index 000000000..477db2a14 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_pipe/tasks/impl.yml @@ -0,0 +1,107 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: ({{select_crypto_backend}}) Create key + openssl_privatekey_pipe: + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: ({{select_crypto_backend}}) Get key info + openssl_privatekey_info: + content: "{{ result.privatekey }}" + register: result_info + +- assert: + that: + - result is changed + - result.privatekey.startswith('----') + - result_info.type == 'RSA' + - result_info.public_data.size == 4096 + - result_info.public_data.exponent >= 5 + +- assert: + that: + - result_info.public_key_fingerprints.sha256 | length > 10 + - result.fingerprint.sha256 == result_info.public_key_fingerprints.sha256 + when: result.fingerprint is not none + +- name: ({{select_crypto_backend}}) Update key (check mode) + openssl_privatekey_pipe: + select_crypto_backend: '{{ select_crypto_backend }}' + content: "{{ result.privatekey }}" + size: '{{ default_rsa_key_size }}' + register: update_check + check_mode: true + +- name: ({{select_crypto_backend}}) Update key (check mode, with return_current_key=true) + openssl_privatekey_pipe: + select_crypto_backend: '{{ select_crypto_backend }}' + content: "{{ result.privatekey }}" + size: '{{ default_rsa_key_size }}' + return_current_key: true + register: update_check_return + check_mode: true + +- name: ({{select_crypto_backend}}) Update key + openssl_privatekey_pipe: + select_crypto_backend: '{{ select_crypto_backend }}' + content: "{{ result.privatekey }}" + size: '{{ default_rsa_key_size }}' + register: update + +- name: ({{select_crypto_backend}}) Update key (idempotent, check mode) + openssl_privatekey_pipe: + select_crypto_backend: '{{ select_crypto_backend }}' + content: "{{ update.privatekey }}" + size: '{{ default_rsa_key_size }}' + register: update_idempotent_check + check_mode: true + +- name: ({{select_crypto_backend}}) Update key (idempotent) + openssl_privatekey_pipe: + select_crypto_backend: '{{ select_crypto_backend }}' + content: "{{ update.privatekey }}" + size: '{{ default_rsa_key_size }}' + register: update_idempotent + +- name: ({{select_crypto_backend}}) Update key (idempotent, check mode, with return_current_key=true) + openssl_privatekey_pipe: + select_crypto_backend: '{{ select_crypto_backend }}' + content: "{{ update.privatekey }}" + size: '{{ default_rsa_key_size }}' + return_current_key: true + register: update_idempotent_return_check + check_mode: true + +- name: ({{select_crypto_backend}}) Update key (idempotent, with return_current_key=true) + openssl_privatekey_pipe: + select_crypto_backend: '{{ select_crypto_backend }}' + content: "{{ update.privatekey }}" + size: '{{ default_rsa_key_size }}' + return_current_key: true + register: update_idempotent_return + +- name: ({{select_crypto_backend}}) Get key info + openssl_privatekey_info: + content: "{{ update.privatekey }}" + register: update_info + +- assert: + that: + - update_check is changed + - update_check.privatekey == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - update_check_return is changed + - update_check_return.privatekey == result.privatekey + - update is changed + - update.privatekey != result.privatekey + - update_info.public_data.size == default_rsa_key_size + - update_idempotent_check is not changed + - update_idempotent_check.privatekey is undefined + - update_idempotent is not changed + - update_idempotent.privatekey is undefined + - update_idempotent_return_check is not changed + - update_idempotent_return_check.privatekey == update.privatekey + - update_idempotent_return is not changed + - update_idempotent_return.privatekey == update.privatekey diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_pipe/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_pipe/tasks/main.yml new file mode 100644 index 000000000..39a2d0ebe --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_privatekey_pipe/tasks/main.yml @@ -0,0 +1,21 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Run module with backend autodetection + openssl_privatekey_pipe: + size: '{{ default_rsa_key_size }}' + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('0.5', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/tasks/impl.yml new file mode 100644 index 000000000..ad59cd8f2 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/tasks/impl.yml @@ -0,0 +1,220 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "({{ select_crypto_backend }}) Generate privatekey" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey.pem' + size: '{{ default_rsa_key_size }}' + +- name: "({{ select_crypto_backend }}) Generate publickey - PEM format (check mode)" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + check_mode: true + register: publickey_check + +- name: "({{ select_crypto_backend }}) Generate publickey - PEM format" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: publickey + +- name: "({{ select_crypto_backend }}) Generate publickey - PEM format (check mode, idempotence)" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + check_mode: true + register: publickey_check2 + +- name: "({{ select_crypto_backend }}) Generate publickey - PEM format (idempotence)" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: publickey_idempotence + +- name: "({{ select_crypto_backend }}) Verify check mode" + assert: + that: + - publickey_check is changed + - publickey is changed + - publickey_check2 is not changed + - publickey_idempotence is not changed + +- name: "({{ select_crypto_backend }}) Generate publickey - OpenSSH format" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey-ssh.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + format: OpenSSH + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.4.0', '>=') + +- name: "({{ select_crypto_backend }}) Generate publickey - OpenSSH format - test idempotence (issue 33256)" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey-ssh.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + format: OpenSSH + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.4.0', '>=') + register: publickey_ssh_idempotence + +- name: "({{ select_crypto_backend }}) Generate publickey2 - standard" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey2.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "({{ select_crypto_backend }}) Delete publickey2 - standard" + openssl_publickey: + state: absent + path: '{{ remote_tmp_dir }}/publickey2.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: publickey2_absent + +- name: "({{ select_crypto_backend }}) Delete publickey2 - standard (idempotence)" + openssl_publickey: + state: absent + path: '{{ remote_tmp_dir }}/publickey2.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: publickey2_absent_idempotence + +- name: "({{ select_crypto_backend }}) Generate privatekey3 - with passphrase" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey3.pem' + passphrase: ansible + cipher: auto + size: '{{ default_rsa_key_size }}' + +- name: "({{ select_crypto_backend }}) Generate publickey3 - with passphrase protected privatekey" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey3.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey3.pem' + privatekey_passphrase: ansible + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "({{ select_crypto_backend }}) Generate publickey3 - with passphrase protected privatekey - idempotence" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey3.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey3.pem' + privatekey_passphrase: ansible + select_crypto_backend: '{{ select_crypto_backend }}' + register: publickey3_idempotence + +- name: "({{ select_crypto_backend }}) Generate empty file that will hold a public key (issue 33072)" + file: + path: '{{ remote_tmp_dir }}/publickey4.pub' + state: touch + +- name: "({{ select_crypto_backend }}) Generate publickey in empty existing file (issue 33072)" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey4.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "({{ select_crypto_backend }}) Generate privatekey 5 (ECC)" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey5.pem' + type: ECC + curve: secp256r1 + size: '{{ default_rsa_key_size }}' + +- name: "({{ select_crypto_backend }}) Generate publickey 5 - PEM format" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey5.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey5_1 +- name: "({{ select_crypto_backend }}) Generate publickey 5 - PEM format (idempotent)" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey5.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey5_2 +- name: "({{ select_crypto_backend }}) Generate publickey 5 - PEM format (different private key)" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey5.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey5.pem' + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: privatekey5_3 + +- name: "({{ select_crypto_backend }}) Generate privatekey with password" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + size: '{{ default_rsa_key_size }}' + +- name: "({{ select_crypto_backend }}) Generate publickey - PEM format (failed passphrase 1)" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey_pw1.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + privatekey_passphrase: hunter2 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: passphrase_error_1 + +- name: "({{ select_crypto_backend }}) Generate publickey - PEM format (failed passphrase 2)" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey_pw2.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + privatekey_passphrase: wrong_password + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: passphrase_error_2 + +- name: "({{ select_crypto_backend }}) Generate publickey - PEM format (failed passphrase 3)" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey_pw3.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: passphrase_error_3 + +- name: "({{ select_crypto_backend }}) Create broken key" + copy: + dest: "{{ remote_tmp_dir }}/publickeybroken.pub" + content: "broken" +- name: "({{ select_crypto_backend }}) Regenerate broken key" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickeybroken.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey5.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: output_broken + +- name: "({{ select_crypto_backend }}) Generate publickey - PEM format (for removal)" + openssl_publickey: + path: '{{ remote_tmp_dir }}/publickey_removal.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' +- name: "({{ select_crypto_backend }}) Generate publickey - PEM format (removal)" + openssl_publickey: + state: absent + path: '{{ remote_tmp_dir }}/publickey_removal.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: remove_1 +- name: "({{ select_crypto_backend }}) Generate publickey - PEM format (removal, idempotent)" + openssl_publickey: + state: absent + path: '{{ remote_tmp_dir }}/publickey_removal.pub' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: remove_2 diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/tasks/main.yml new file mode 100644 index 000000000..50eb74db0 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/tasks/main.yml @@ -0,0 +1,31 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- block: + - name: Generate privatekey1 - standard + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_autodetect.pem' + size: '{{ default_rsa_key_size }}' + + - name: Run module with backend autodetection + openssl_publickey: + path: '{{ remote_tmp_dir }}/privatekey_autodetect_public.pem' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_autodetect.pem' + + - 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.3', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/tests/validate.yml new file mode 100644 index 000000000..8a1ab86e3 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey/tests/validate.yml @@ -0,0 +1,155 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "({{ select_crypto_backend }}) Read publickey 1" + slurp: + src: '{{ remote_tmp_dir }}/publickey.pub' + register: slurp + +- name: "({{ select_crypto_backend }}) Validate publickey 1 idempotence and result behavior" + assert: + that: + - publickey is changed + - publickey_idempotence is not changed + - publickey.publickey == (slurp.content | b64decode) + - publickey.publickey == publickey_idempotence.publickey + +- name: "({{ select_crypto_backend }}) Validate public key (test - privatekey modulus)" + shell: '{{ openssl_binary }} rsa -noout -modulus -in {{ remote_tmp_dir }}/privatekey.pem' + register: privatekey_modulus + +- name: "({{ select_crypto_backend }}) Validate public key (test - publickey modulus)" + shell: '{{ openssl_binary }} rsa -pubin -noout -modulus < {{ remote_tmp_dir }}/publickey.pub' + register: publickey_modulus + +- name: "({{ select_crypto_backend }}) Validate public key (assert)" + assert: + that: + - publickey_modulus.stdout == privatekey_modulus.stdout + +- name: "({{ select_crypto_backend }}) Validate public key - OpenSSH format (test - privatekey's publickey)" + shell: 'ssh-keygen -y -f {{ remote_tmp_dir }}/privatekey.pem' + register: privatekey_publickey + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.4.0', '>=') + +- name: "({{ select_crypto_backend }}) Validate public key - OpenSSH format (test - publickey)" + slurp: + src: '{{ remote_tmp_dir }}/publickey-ssh.pub' + register: publickey + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.4.0', '>=') + +- name: "({{ select_crypto_backend }}) Validate public key - OpenSSH format (assert)" + assert: + that: + - privatekey_publickey.stdout == '{{ publickey.content|b64decode }}' + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.4.0', '>=') + +- name: "({{ select_crypto_backend }}) Validate public key - OpenSSH format - test idempotence (issue 33256)" + assert: + that: + - publickey_ssh_idempotence is not changed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('1.4.0', '>=') + +- name: "({{ select_crypto_backend }}) Validate publickey2 (test - Ensure key has been removed)" + stat: + path: '{{ remote_tmp_dir }}/publickey2.pub' + register: publickey2 + +- name: "({{ select_crypto_backend }}) Validate publickey2 (assert - Ensure key has been removed)" + assert: + that: + - publickey2.stat.exists == False + +- name: "({{ select_crypto_backend }}) Validate publickey2 removal behavior" + assert: + that: + - publickey2_absent is changed + - publickey2_absent_idempotence is not changed + - publickey2_absent.publickey is none + + +- name: "({{ select_crypto_backend }}) Validate publickey3 (test - privatekey modulus)" + shell: '{{ openssl_binary }} rsa -noout -modulus -in {{ remote_tmp_dir }}/privatekey3.pem -passin pass:ansible' + register: privatekey3_modulus + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: "({{ select_crypto_backend }}) Validate publickey3 (test - publickey modulus)" + shell: '{{ openssl_binary }} rsa -pubin -noout -modulus < {{ remote_tmp_dir }}/publickey3.pub' + register: publickey3_modulus + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: "({{ select_crypto_backend }}) Validate publickey3 (assert)" + assert: + that: + - publickey3_modulus.stdout == privatekey3_modulus.stdout + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: "({{ select_crypto_backend }}) Validate publickey3 idempotence (assert)" + assert: + that: + - publickey3_idempotence is not changed + +- name: "({{ select_crypto_backend }}) Validate publickey4 (test - privatekey modulus)" + shell: '{{ openssl_binary }} rsa -noout -modulus -in {{ remote_tmp_dir }}/privatekey.pem' + register: privatekey4_modulus + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: "({{ select_crypto_backend }}) Validate publickey4 (test - publickey modulus)" + shell: '{{ openssl_binary }} rsa -pubin -noout -modulus < {{ remote_tmp_dir }}/publickey4.pub' + register: publickey4_modulus + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: "({{ select_crypto_backend }}) Validate publickey4 (assert)" + assert: + that: + - publickey4_modulus.stdout == privatekey4_modulus.stdout + when: openssl_version.stdout is version('0.9.8zh', '>=') + +- name: "({{ select_crypto_backend }}) Validate idempotency and backup" + assert: + that: + - privatekey5_1 is changed + - privatekey5_1.backup_file is undefined + - privatekey5_2 is not changed + - privatekey5_2.backup_file is undefined + - privatekey5_3 is changed + - privatekey5_3.backup_file is string + +- name: "({{ select_crypto_backend }}) Validate public key 5 (test - privatekey's pubkey)" + command: '{{ openssl_binary }} ec -in {{ remote_tmp_dir }}/privatekey5.pem -pubout' + register: privatekey5_pubkey + +- name: "({{ select_crypto_backend }}) Validate public key 5 (test - publickey pubkey)" + # Fancy way of writing "cat {{ remote_tmp_dir }}/publickey5.pub" + command: '{{ openssl_binary }} ec -pubin -in {{ remote_tmp_dir }}/publickey5.pub -pubout' + register: publickey5_pubkey + +- name: "({{ select_crypto_backend }}) Validate public key 5 (assert)" + assert: + that: + - publickey5_pubkey.stdout == privatekey5_pubkey.stdout + +- name: + assert: + that: + - passphrase_error_1 is failed + - "'assphrase' in passphrase_error_1.msg or 'assword' in passphrase_error_1.msg" + - passphrase_error_2 is failed + - "'assphrase' in passphrase_error_2.msg or 'assword' in passphrase_error_2.msg or 'serializ' in passphrase_error_2.msg" + - passphrase_error_3 is failed + - "'assphrase' in passphrase_error_3.msg or 'assword' in passphrase_error_3.msg or 'serializ' in passphrase_error_3.msg" + +- name: "({{ select_crypto_backend }}) Verify that broken key will be regenerated" + assert: + that: + - output_broken is changed + +- name: "({{ select_crypto_backend }}) Validate remove" + assert: + that: + - remove_1 is changed + - remove_2 is not changed + - remove_1.backup_file is string + - remove_2.backup_file is undefined diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey_info/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey_info/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey_info/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey_info/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey_info/meta/main.yml new file mode 100644 index 000000000..7c2b42405 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey_info/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir + - prepare_jinja2_compat diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey_info/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey_info/tasks/impl.yml new file mode 100644 index 000000000..016f63752 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey_info/tasks/impl.yml @@ -0,0 +1,92 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- debug: + msg: "Executing tests with backend {{ select_crypto_backend }}" + +- name: ({{select_crypto_backend}}) Get key 1 info + openssl_publickey_info: + path: '{{ remote_tmp_dir }}/publickey_1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that RSA key info is ok + assert: + that: + - "'fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + +- name: ({{select_crypto_backend}}) Read file + slurp: + src: '{{ remote_tmp_dir }}/publickey_1.pem' + register: slurp + +- name: ({{select_crypto_backend}}) Get key 1 info directly + openssl_publickey_info: + content: '{{ slurp.content | b64decode }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_direct + +- name: ({{select_crypto_backend}}) Compare output of direct and loaded info + assert: + that: + - result == result_direct + +- name: ({{select_crypto_backend}}) Get key 2 info + openssl_publickey_info: + path: '{{ remote_tmp_dir }}/publickey_2.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that RSA key info is ok + assert: + that: + - "'fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "result.public_data.size == default_rsa_key_size" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + +- name: ({{select_crypto_backend}}) Get key 3 info + openssl_publickey_info: + path: '{{ remote_tmp_dir }}/publickey_3.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that ECC key info is ok + assert: + that: + - "'fingerprints' in result" + - "'type' in result" + - "result.type == 'ECC'" + - "'public_data' in result" + - "result.public_data.curve is string" + - "result.public_data.x != 0" + - "result.public_data.y != 0" + - "result.public_data.exponent_size == (521 if (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') else 256)" + +- name: ({{select_crypto_backend}}) Get key 4 info + openssl_publickey_info: + path: '{{ remote_tmp_dir }}/publickey_4.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check that DSA key info is ok + assert: + that: + - "'fingerprints' in result" + - "'type' in result" + - "result.type == 'DSA'" + - "'public_data' in result" + - "result.public_data.p > 2" + - "result.public_data.q > 2" + - "result.public_data.g >= 2" + - "result.public_data.y > 2" diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey_info/tasks/main.yml new file mode 100644 index 000000000..b266086d1 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_publickey_info/tasks/main.yml @@ -0,0 +1,49 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Generate privatekey 1 + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_1.pem' + +- name: Generate privatekey 2 (less bits) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_2.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + +- name: Generate privatekey 3 (ECC) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_3.pem' + type: ECC + curve: "{{ (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') | ternary('secp521r1', 'secp256k1') }}" + # ^ cryptography on CentOS6 doesn't support secp256k1, so we use secp521r1 instead + select_crypto_backend: cryptography + +- name: Generate privatekey 4 (DSA) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_4.pem' + type: DSA + size: 1024 + +- name: Generate public keys + openssl_publickey: + privatekey_path: '{{ remote_tmp_dir }}/privatekey_{{ item }}.pem' + path: '{{ remote_tmp_dir }}/publickey_{{ item }}.pem' + loop: + - 1 + - 2 + - 3 + - 4 + +- name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + when: cryptography_version.stdout is version('1.2.3', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_signature/aliases b/ansible_collections/community/crypto/tests/integration/targets/openssl_signature/aliases new file mode 100644 index 000000000..f0a0ba7bc --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_signature/aliases @@ -0,0 +1,8 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +openssl_signature_info +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_signature/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_signature/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_signature/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_signature/tasks/loop.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_signature/tasks/loop.yml new file mode 100644 index 000000000..d86b7ca5e --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_signature/tasks/loop.yml @@ -0,0 +1,32 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# This file is intended to be included in a loop statement +- name: Sign statement with {{ item.type }} key - {{ item.passwd }} using {{ item.backend }} + openssl_signature: + privatekey_path: '{{ remote_tmp_dir }}/{{item.backend}}_privatekey_{{ item.type }}_{{ item.passwd }}.pem' + privatekey_passphrase: '{{ item.privatekey_passphrase | default(omit) }}' + path: '{{ remote_tmp_dir }}/statement.txt' + select_crypto_backend: '{{ item.backend }}' + register: sign_result + +- debug: + var: sign_result + +- name: Verify {{ item.type }} signature - {{ item.passwd }} using {{ item.backend }} + openssl_signature_info: + certificate_path: '{{ remote_tmp_dir }}/{{item.backend}}_certificate_{{ item.type }}_{{ item.passwd }}.pem' + path: '{{ remote_tmp_dir }}/statement.txt' + signature: '{{ sign_result.signature }}' + select_crypto_backend: '{{ item.backend }}' + register: verify_result + +- name: Make sure the signature is valid + assert: + that: + - verify_result.valid + +- debug: + var: verify_result diff --git a/ansible_collections/community/crypto/tests/integration/targets/openssl_signature/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/openssl_signature/tasks/main.yml new file mode 100644 index 000000000..f9ed1dec6 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/openssl_signature/tasks/main.yml @@ -0,0 +1,109 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Test matrix: +# * cryptography +# * DSA or ECC or ... +# * password protected private key or not + +- name: Set up test combinations + set_fact: + all_tests: [] + backends: [] + key_types: [] + key_password: + - passwd: nopasswd + - passwd: passwd + privatekey_passphrase: hunter2 + privatekey_cipher: auto + +- name: Add cryptography backend + set_fact: + backends: "{{ backends + [ { 'backend': 'cryptography' } ] }}" + when: cryptography_version.stdout is version('1.4', '>=') + +- name: Add RSA tests + set_fact: + key_types: "{{ key_types + [ { 'type': 'RSA', 'size': default_rsa_key_size } ] }}" + when: cryptography_version.stdout is version('1.4', '>=') + +- name: Add DSA + ECDSA tests + set_fact: + key_types: "{{ key_types + [ { 'type': 'DSA', 'size': 2048 }, { 'type': 'ECC', 'curve': 'secp256r1' } ] }}" + when: + - cryptography_version.stdout is version('1.5', '>=') + # FreeBSD 11 fails on secp256r1 keys + - not ansible_os_family == 'FreeBSD' + +- name: Add Ed25519 + Ed448 tests + set_fact: + key_types: "{{ key_types + [ { 'type': 'Ed25519' }, { 'type': 'Ed448' } ] }}" + when: + # The module under tests works with >= 2.6, but we also need to be able to create a certificate which requires 2.8 + - cryptography_version.stdout is version('2.8', '>=') + # FreeBSD doesn't have support for Ed448/25519 + - not ansible_os_family == 'FreeBSD' + +- name: Create all test combinations + set_fact: + # Explanation: see https://serverfault.com/a/1004124 + all_tests: >- + [ + {% for b in backends %} + {% for kt in key_types %} + {% for kp in key_password %} + {{ b | combine (kt) | combine(kp) }}, + {% endfor %} + {% endfor %} + {% endfor %} + ] + +- name: Generate private keys + openssl_privatekey: + path: '{{ remote_tmp_dir }}/{{item.backend}}_privatekey_{{ item.type }}_{{ item.passwd }}.pem' + type: '{{ item.type }}' + curve: '{{ item.curve | default(omit) }}' + size: '{{ item.size | default(omit) }}' + passphrase: '{{ item.privatekey_passphrase | default(omit) }}' + cipher: '{{ item.privatekey_cipher | default(omit) }}' + select_crypto_backend: cryptography + loop: '{{ all_tests }}' + +- name: Generate public keys + openssl_publickey: + path: '{{ remote_tmp_dir }}/{{item.backend}}_publickey_{{ item.type }}_{{ item.passwd }}.pem' + privatekey_path: '{{ remote_tmp_dir }}/{{item.backend}}_privatekey_{{ item.type }}_{{ item.passwd }}.pem' + privatekey_passphrase: '{{ item.privatekey_passphrase | default(omit) }}' + loop: '{{ all_tests }}' + +- name: Generate CSRs + openssl_csr: + path: '{{ remote_tmp_dir }}/{{item.backend}}_{{ item.type }}_{{ item.passwd }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/{{item.backend}}_privatekey_{{ item.type }}_{{ item.passwd }}.pem' + privatekey_passphrase: '{{ item.privatekey_passphrase | default(omit) }}' + loop: '{{ all_tests }}' + +- name: Generate selfsigned certificates + x509_certificate: + provider: selfsigned + path: '{{ remote_tmp_dir }}/{{item.backend}}_certificate_{{ item.type }}_{{ item.passwd }}.pem' + privatekey_path: '{{ remote_tmp_dir }}/{{item.backend}}_privatekey_{{ item.type }}_{{ item.passwd }}.pem' + privatekey_passphrase: '{{ item.privatekey_passphrase | default(omit) }}' + csr_path: '{{ remote_tmp_dir }}/{{item.backend}}_{{ item.type }}_{{ item.passwd }}.csr' + loop: '{{ all_tests }}' + +- name: Create statement to be signed + copy: + content: "Erst wenn der Subwoofer die Katze inhaliert, fickt der Bass richtig übel. -- W.A. Mozart" + dest: '{{ remote_tmp_dir }}/statement.txt' + +- name: Loop over all variants + include_tasks: loop.yml + loop: '{{ all_tests }}' diff --git a/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/defaults/main.yml b/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/defaults/main.yml new file mode 100644 index 000000000..51ee6d5e1 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/defaults/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +badssl_host: wrong.host.badssl.com +httpbin_host: httpbin.org +sni_host: ci-files.testing.ansible.com +badssl_host_substring: wrong.host.badssl.com diff --git a/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/meta/main.yml new file mode 100644 index 000000000..982de6eb0 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/meta/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/tasks/default.yml b/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/tasks/default.yml new file mode 100644 index 000000000..562fadd2e --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/tasks/default.yml @@ -0,0 +1,75 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: RedHat - Enable the dynamic CA configuration feature + command: update-ca-trust force-enable + when: ansible_os_family == 'RedHat' + +- name: RedHat - Retrieve test cacert + get_url: + url: "http://ansible.http.tests/cacert.pem" + dest: "/etc/pki/ca-trust/source/anchors/ansible.pem" + when: ansible_os_family == 'RedHat' + +- name: Get client cert/key + get_url: + url: "http://ansible.http.tests/{{ item }}" + dest: "{{ remote_tmp_dir }}/{{ item }}" + with_items: + - client.pem + - client.key + +- name: Suse - Retrieve test cacert + get_url: + url: "http://ansible.http.tests/cacert.pem" + dest: "/etc/pki/trust/anchors/ansible.pem" + when: ansible_os_family == 'Suse' + +- name: Debian - Retrieve test cacert + get_url: + url: "http://ansible.http.tests/cacert.pem" + dest: "/usr/local/share/ca-certificates/ansible.crt" + when: ansible_os_family == 'Debian' + +- name: Redhat - Update ca trust + command: update-ca-trust extract + when: ansible_os_family == 'RedHat' + +- name: Debian/Suse - Update ca certificates + command: update-ca-certificates + when: ansible_os_family == 'Debian' or ansible_os_family == 'Suse' + +- name: FreeBSD - Retrieve test cacert + get_url: + url: "http://ansible.http.tests/cacert.pem" + dest: "/tmp/ansible.pem" + when: ansible_os_family == 'FreeBSD' + +- name: FreeBSD - Read test cacert + slurp: + src: "/tmp/ansible.pem" + register: slurp + when: ansible_os_family == 'FreeBSD' + +- name: FreeBSD - Add cacert to root certificate store + blockinfile: + path: "/etc/ssl/cert.pem" + block: "{{ slurp.content | b64decode }}" + when: ansible_os_family == 'FreeBSD' + +- name: MacOS - Retrieve test cacert + when: ansible_os_family == 'Darwin' + block: + - uri: + url: "http://ansible.http.tests/cacert.pem" + return_content: true + register: cacert_pem + + - raw: '{{ ansible_python_interpreter }} -c "import ssl; print(ssl.get_default_verify_paths().cafile)"' + register: macos_cafile + + - blockinfile: + path: "{{ macos_cafile.stdout_lines|first }}" + block: "{{ cacert_pem.content }}" diff --git a/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/tasks/main.yml new file mode 100644 index 000000000..bd5be7db2 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/tasks/main.yml @@ -0,0 +1,32 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# The docker --link functionality gives us an ENV var we can key off of to see if we have access to +# the httptester container +- set_fact: + has_httptester: "{{ lookup('env', 'HTTPTESTER') != '' }}" + +- name: make sure we have the ansible_os_family and ansible_distribution_version facts + setup: + gather_subset: distribution + when: ansible_facts == {} + +# If we are running with access to a httptester container, grab it's cacert and install it +- block: + # Override hostname defaults with httptester linked names + - include_vars: httptester.yml + + - include_tasks: "{{ lookup('first_found', files)}}" + vars: + files: + - "{{ ansible_os_family | lower }}.yml" + - "default.yml" + when: + - has_httptester|bool diff --git a/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/vars/httptester.yml b/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/vars/httptester.yml new file mode 100644 index 000000000..1ea7424fd --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/prepare_http_tests/vars/httptester.yml @@ -0,0 +1,10 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# these are fake hostnames provided by docker link for the httptester container +badssl_host: fail.ansible.http.tests +httpbin_host: ansible.http.tests +sni_host: sni1.ansible.http.tests +badssl_host_substring: HTTP Client Testing Service diff --git a/ansible_collections/community/crypto/tests/integration/targets/prepare_jinja2_compat/filter_plugins/jinja_compatibility.py b/ansible_collections/community/crypto/tests/integration/targets/prepare_jinja2_compat/filter_plugins/jinja_compatibility.py new file mode 100644 index 000000000..87ce01dce --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/prepare_jinja2_compat/filter_plugins/jinja_compatibility.py @@ -0,0 +1,139 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Copyright 2007 Pallets +# +# 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. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 + +from jinja2.filters import contextfilter +from jinja2.runtime import Undefined +from jinja2.exceptions import TemplateRuntimeError, FilterArgumentError + +try: + from jinja2.nodes import EvalContext + HAS_EVALCONTEXT = True +except ImportError: + HAS_EVALCONTEXT = False + + +def call_test(environment, test_name, value, args, kwargs): + try: + return environment.call_test(test_name, value, args, kwargs) + except AttributeError: + # call_test was added together with selectattr... + func = environment.tests.get(test_name) + if func is None: + raise TemplateRuntimeError('no test named %r' % test_name) + return func(value, *args, **kwargs) + + +def call_filter(environment, name, value, args=None, kwargs=None, + context=None, eval_ctx=None): + func = environment.filters.get(name) + if func is None: + raise TemplateRuntimeError('no filter named %r' % name) + args = list(args or ()) + if getattr(func, 'contextfilter', False): + if context is None: + raise TemplateRuntimeError('Attempted to invoke context filter without context') + args.insert(0, context) + elif getattr(func, 'evalcontextfilter', False): + if eval_ctx is None: + if context is not None: + eval_ctx = context.eval_ctx + elif HAS_EVALCONTEXT: + eval_ctx = EvalContext(environment) + else: + raise TemplateRuntimeError('Too old Jinja2 does not have EvalContext') + args.insert(0, eval_ctx) + elif getattr(func, 'environmentfilter', False): + args.insert(0, environment) + return func(value, *args, **(kwargs or {})) + + +def make_attrgetter(environment, attribute_str, default=None): + attributes = [int(attribute) if attribute.isdigit() else attribute for attribute in attribute_str.split(".")] + + def f(item): + for attribute in attributes: + item = environment.getitem(item, attribute) + if default and isinstance(item, Undefined): + item = default + return item + + return f + + +@contextfilter +def compatibility_selectattr_filter(context, sequence, attribute_str, test_name, *args, **kwargs): + f = make_attrgetter(context.environment, attribute_str) + for item in sequence: + if call_test(context.environment, test_name, f(item), args, kwargs): + yield item + + +def prepare_map(context, args, kwargs): + if len(args) == 0 and "attribute" in kwargs: + attribute = kwargs.pop("attribute") + default = kwargs.pop("default", None) + if kwargs: + raise FilterArgumentError("Unexpected keyword argument {0!r}".format(next(iter(kwargs)))) + func = make_attrgetter(context.environment, attribute, default=default) + else: + try: + name = args[0] + args = args[1:] + except LookupError: + raise FilterArgumentError("map requires a filter argument") + + def func(item): + return call_filter(context.environment, name, item, args, kwargs, context=context) + + return func + + +@contextfilter +def compatibility_map_filter(context, seq, *args, **kwargs): + func = prepare_map(context, args, kwargs) + if seq: + for item in seq: + yield func(item) + + +class FilterModule: + ''' Jinja2 compat filters ''' + + def filters(self): + return { + 'selectattr': compatibility_selectattr_filter, + 'map': compatibility_map_filter, + } diff --git a/ansible_collections/community/crypto/tests/integration/targets/prepare_jinja2_compat/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/prepare_jinja2_compat/tasks/main.yml new file mode 100644 index 000000000..f55df21f8 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/prepare_jinja2_compat/tasks/main.yml @@ -0,0 +1,4 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/ansible_collections/community/crypto/tests/integration/targets/prepare_jinja2_compat/test_plugins/jinja_compatibility.py b/ansible_collections/community/crypto/tests/integration/targets/prepare_jinja2_compat/test_plugins/jinja_compatibility.py new file mode 100644 index 000000000..9b1ae919d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/prepare_jinja2_compat/test_plugins/jinja_compatibility.py @@ -0,0 +1,24 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +def compatibility_equalto_test(a, b): + return a == b + + +def compatibility_in_test(a, b): + return a in b + + +class TestModule: + ''' Ansible math jinja2 tests ''' + + def tests(self): + return { + 'equalto': compatibility_equalto_test, + 'in': compatibility_in_test, + } diff --git a/ansible_collections/community/crypto/tests/integration/targets/prepare_tests/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/prepare_tests/tasks/main.yml new file mode 100644 index 000000000..f55df21f8 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/prepare_tests/tasks/main.yml @@ -0,0 +1,4 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_acme/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_acme/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_acme/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_acme/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_acme/tasks/main.yml new file mode 100644 index 000000000..d8d70cb90 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_acme/tasks/main.yml @@ -0,0 +1,12 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- debug: + msg: "ACME test container IP is {{ acme_host }}; OpenSSL version is {{ openssl_version.stdout }}; cryptography version is {{ cryptography_version.stdout }}" diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_acme/tasks/obtain-cert.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_acme/tasks/obtain-cert.yml new file mode 100644 index 000000000..6882e5339 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_acme/tasks/obtain-cert.yml @@ -0,0 +1,159 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +## PRIVATE KEY ################################################################################ +- name: ({{ certgen_title }}) Create cert private key + openssl_privatekey: + path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" + type: "{{ 'RSA' if key_type == 'rsa' else 'ECC' }}" + size: "{{ rsa_bits if key_type == 'rsa' else omit }}" + curve: >- + {{ omit if key_type == 'rsa' else + 'secp256r1' if key_type == 'ec256' else + 'secp384r1' if key_type == 'ec384' else + 'secp521r1' if key_type == 'ec521' else + 'invalid value for key_type!' }} + passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}" + cipher: "{{ 'auto' if certificate_passphrase | default() else omit }}" + force: true +## CSR ######################################################################################## +- name: ({{ certgen_title }}) Create cert CSR + openssl_csr: + path: "{{ remote_tmp_dir }}/{{ certificate_name }}.csr" + privatekey_path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" + privatekey_passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}" + subject_alt_name: "{{ subject_alt_name }}" + subject_alt_name_critical: "{{ subject_alt_name_critical }}" + return_content: true + register: csr_result +## ACME STEP 1 ################################################################################ +- name: ({{ certgen_title }}) Obtain cert, step 1 + acme_certificate: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}" + account_key_content: "{{ account_key_content | default(omit) }}" + account_key_passphrase: "{{ account_key_passphrase | default(omit) | default(omit, true) }}" + modify_account: "{{ modify_account }}" + csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}" + csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}" + dest: "{{ remote_tmp_dir }}/{{ certificate_name }}.pem" + fullchain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-fullchain.pem" + chain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-chain.pem" + challenge: "{{ challenge }}" + deactivate_authzs: "{{ deactivate_authzs }}" + force: "{{ force }}" + remaining_days: "{{ remaining_days }}" + terms_agreed: "{{ terms_agreed }}" + account_email: "{{ account_email }}" + register: challenge_data +- name: ({{ certgen_title }}) Print challenge data + debug: + var: challenge_data +- name: ({{ certgen_title }}) Create HTTP challenges + uri: + url: "http://{{ acme_host }}:5000/http/{{ item.key }}/{{ item.value['http-01'].resource[('.well-known/acme-challenge/'|length):] }}" + method: PUT + body_format: raw + body: "{{ item.value['http-01'].resource_value }}" + headers: + content-type: "application/octet-stream" + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'http-01'" +- name: ({{ certgen_title }}) Create DNS challenges + uri: + url: "http://{{ acme_host }}:5000/dns/{{ item.key }}" + method: PUT + body_format: json + body: "{{ item.value }}" + with_dict: "{{ challenge_data.challenge_data_dns }}" + when: "challenge_data is changed and challenge == 'dns-01'" +- name: ({{ certgen_title }}) Create TLS ALPN challenges (acme_challenge_cert_helper) + acme_challenge_cert_helper: + challenge: tls-alpn-01 + challenge_data: "{{ item.value['tls-alpn-01'] }}" + private_key_src: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" + private_key_passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}" + with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper') else {} }}" + register: tls_alpn_challenges + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Read private key + slurp: + src: '{{ remote_tmp_dir }}/{{ certificate_name }}.key' + register: slurp + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Set TLS ALPN challenges (acme_challenge_cert_helper) + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.domain }}/{{ item.identifier }}/certificate-and-key" + method: PUT + body_format: raw + body: "{{ item.challenge_certificate }}\n{{ slurp.content | b64decode }}" + headers: + content-type: "application/pem-certificate-chain" + with_items: "{{ tls_alpn_challenges.results if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper') else [] }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')" +- name: ({{ certgen_title }}) Create TLS ALPN challenges (der-value-b64) + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}/{{ item.value['tls-alpn-01'].resource_original }}/der-value-b64" + method: PUT + body_format: raw + body: "{{ item.value['tls-alpn-01'].resource_value }}" + headers: + content-type: "application/octet-stream" + with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'der-value-b64') else {} }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'der-value-b64')" +## ACME STEP 2 ################################################################################ +- name: ({{ certgen_title }}) Obtain cert, step 2 + acme_certificate: + select_crypto_backend: "{{ select_crypto_backend }}" + acme_version: 2 + acme_directory: https://{{ acme_host }}:14000/dir + validate_certs: false + account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}" + account_key_content: "{{ account_key_content | default(omit) }}" + account_key_passphrase: "{{ account_key_passphrase | default(omit) | default(omit, true) }}" + account_uri: "{{ challenge_data.account_uri }}" + modify_account: "{{ modify_account }}" + csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}" + csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}" + dest: "{{ remote_tmp_dir }}/{{ certificate_name }}.pem" + fullchain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-fullchain.pem" + chain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-chain.pem" + challenge: "{{ challenge }}" + deactivate_authzs: "{{ deactivate_authzs }}" + force: "{{ force }}" + remaining_days: "{{ remaining_days }}" + terms_agreed: "{{ terms_agreed }}" + account_email: "{{ account_email }}" + data: "{{ challenge_data }}" + retrieve_all_alternates: "{{ retrieve_all_alternates | default(omit) }}" + select_chain: "{{ select_chain | default(omit) if select_crypto_backend == 'cryptography' else omit }}" + register: certificate_obtain_result + when: challenge_data is changed +- name: ({{ certgen_title }}) Deleting HTTP challenges + uri: + url: "http://{{ acme_host }}:5000/http/{{ item.key }}/{{ item.value['http-01'].resource[('.well-known/acme-challenge/'|length):] }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'http-01'" +- name: ({{ certgen_title }}) Deleting DNS challenges + uri: + url: "http://{{ acme_host }}:5000/dns/{{ item.key }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data_dns }}" + when: "challenge_data is changed and challenge == 'dns-01'" +- name: ({{ certgen_title }}) Deleting TLS ALPN challenges + uri: + url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}" + method: DELETE + with_dict: "{{ challenge_data.challenge_data }}" + when: "challenge_data is changed and challenge == 'tls-alpn-01'" +- name: ({{ certgen_title }}) Get root certificate + get_url: + url: "http://{{ acme_host }}:5000/root-certificate-for-ca/{{ acme_expected_root_number | default(0) if select_crypto_backend == 'cryptography' else 0 }}" + dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-root.pem" +############################################################################################### diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_bcrypt/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_bcrypt/meta/main.yml new file mode 100644 index 000000000..d4a5c7d05 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_bcrypt/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_remote_constraints + - setup_pkg_mgr diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_bcrypt/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_bcrypt/tasks/main.yml new file mode 100644 index 000000000..0e5996849 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_bcrypt/tasks/main.yml @@ -0,0 +1,28 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Attempt to install dependencies for OpenSSH > 7.8 + block: + - name: Ensure bcrypt 3.1.5 available + become: true + pip: + name: bcrypt==3.1.5 + extra_args: "-c {{ remote_constraints }}" + + - name: Register bcrypt version + command: "{{ ansible_python.executable }} -c 'import bcrypt; print(bcrypt.__version__)'" + register: bcrypt_version + ignore_errors: true + +- name: Ensure bcrypt_version is defined + set_fact: + bcrypt_version: + stdout: "0.0" + when: bcrypt_version is failed diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/meta/main.yml new file mode 100644 index 000000000..b9b2b3b5d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_python_info + - setup_remote_constraints + - setup_pkg_mgr diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/tasks/main.yml new file mode 100644 index 000000000..83da50c8f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/tasks/main.yml @@ -0,0 +1,124 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Register system environment + command: "{{ ansible_python.executable }} -c 'import os; print(dict(os.environ))'" + register: sys_environment + +- name: Show system environment + debug: + var: sys_environment.stdout_lines + +- name: Default value for OpenSSL binary path + set_fact: + openssl_binary: openssl + +- name: Include OS-specific variables + include_vars: '{{ ansible_os_family }}.yml' + when: not ansible_os_family == "Darwin" + +- name: Check whether OpenSSL is there + command: "{{ openssl_binary }} version" + register: openssl_version_full + ignore_errors: true + +- name: Install OpenSSL + become: true + package: + name: '{{ openssl_package_name }}' + when: not ansible_os_family == 'Darwin' and openssl_version_full is failed + +- name: Register openssl version (full) + command: "{{ openssl_binary }} version" + register: openssl_version_full + +- name: Show openssl version (full) + debug: + var: openssl_version_full.stdout_lines + +- when: ansible_os_family == "Darwin" and "LibreSSL" in openssl_version_full.stdout + # In case LibreSSL is installed on macOS, we need to install a more modern OpenSSL + block: + - name: MACOS | Find brew binary + command: which brew + register: brew_which + + - name: MACOS | Get owner of brew binary + stat: + path: "{{ brew_which.stdout }}" + register: brew_stat + + - name: MACOS | Install openssl + homebrew: + name: openssl + state: present + become: true + become_user: "{{ brew_stat.stat.pw_name }}" + + - name: MACOS | Locale openssl binary + command: brew --prefix openssl + become: true + become_user: "{{ brew_stat.stat.pw_name }}" + register: brew_openssl_prefix + + - name: MACOS | Point to OpenSSL binary + set_fact: + openssl_binary: "{{ brew_openssl_prefix.stdout }}/bin/openssl" + + - name: MACOS | Register openssl version (full) + command: "{{ openssl_binary }} version" + register: openssl_version_full_again + # We must use a different variable to prevent the 'when' condition of the surrounding block to fail + + - name: MACOS | Show openssl version (full) + debug: + var: openssl_version_full_again.stdout_lines + +- name: Register openssl version + shell: "{{ openssl_binary }} version | cut -d' ' -f2" + register: openssl_version + +- when: ansible_facts.distribution ~ ansible_facts.distribution_major_version not in ['CentOS6', 'RedHat6'] + block: + + - name: Install from system packages + when: ansible_os_family != "Darwin" and target_system_python + block: + + - name: Install cryptography (Python 3 from system packages) + become: true + package: + name: '{{ cryptography_package_name_python3 }}' + when: ansible_python_version is version('3.0', '>=') + + - name: Install cryptography (Python 2 from system packages) + become: true + package: + name: '{{ cryptography_package_name }}' + when: ansible_python_version is version('3.0', '<') + + - name: Install from PyPi + when: ansible_os_family == "Darwin" or not target_system_python + block: + + - name: Install cryptography (PyPi) + become: true + pip: + name: 'cryptography{% if ansible_os_family == "Darwin" %}>=3.3{% endif %}' + state: "{{ 'latest' if not target_system_python_cannot_upgrade_cryptography else omit }}" + extra_args: "-c {{ remote_constraints }}" + +- name: Register cryptography version + command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'" + register: cryptography_version + +- name: Print default key sizes + debug: + msg: "Default RSA key size: {{ default_rsa_key_size }} (for certificates: {{ default_rsa_key_size_certifiates }})" diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/Alpine.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/Alpine.yml new file mode 100644 index 000000000..bb13d46f1 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/Alpine.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +openssl_package_name: openssl +cryptography_package_name: py-cryptography +cryptography_package_name_python3: py3-cryptography diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/Archlinux.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/Archlinux.yml new file mode 100644 index 000000000..81d64a9aa --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/Archlinux.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +openssl_package_name: openssl +cryptography_package_name: python-cryptography +cryptography_package_name_python3: python-cryptography diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/Debian.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/Debian.yml new file mode 100644 index 000000000..6609983a2 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/Debian.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +openssl_package_name: openssl +cryptography_package_name: python-cryptography +cryptography_package_name_python3: python3-cryptography diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/FreeBSD.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/FreeBSD.yml new file mode 100644 index 000000000..1b6bdd8bb --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/FreeBSD.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +openssl_package_name: openssl +cryptography_package_name: py27-cryptography +cryptography_package_name_python3: "py{{ ansible_python.version.major }}{{ ansible_python.version.minor }}-cryptography" diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/RedHat.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/RedHat.yml new file mode 100644 index 000000000..6609983a2 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/RedHat.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +openssl_package_name: openssl +cryptography_package_name: python-cryptography +cryptography_package_name_python3: python3-cryptography diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/Suse.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/Suse.yml new file mode 100644 index 000000000..6609983a2 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/Suse.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +openssl_package_name: openssl +cryptography_package_name: python-cryptography +cryptography_package_name_python3: python3-cryptography diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/main.yml new file mode 100644 index 000000000..c26148e71 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_openssl/vars/main.yml @@ -0,0 +1,13 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +default_rsa_key_size: 1024 +default_rsa_key_size_certifiates: >- + {{ + 2048 if + (ansible_os_family == "RedHat" and ansible_facts.distribution_major_version | int >= 8) or + (ansible_distribution == "Ubuntu" and ansible_facts.distribution_major_version | int >= 20) + else 1024 + }} diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_pkg_mgr/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_pkg_mgr/tasks/main.yml new file mode 100644 index 000000000..e4cb1b602 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_pkg_mgr/tasks/main.yml @@ -0,0 +1,21 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- set_fact: + pkg_mgr: community.general.pkgng + ansible_pkg_mgr: community.general.pkgng + cacheable: true + when: ansible_os_family == 'FreeBSD' and ansible_version.string is version('2.10', '>=') + +- set_fact: + pkg_mgr: community.general.zypper + ansible_pkg_mgr: community.general.zypper + cacheable: true + when: ansible_os_family == 'Suse' and ansible_version.string is version('2.10', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/defaults/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/defaults/main.yml new file mode 100644 index 000000000..33e171d0c --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/defaults/main.yml @@ -0,0 +1,6 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +has_pyopenssl: true diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/meta/main.yml new file mode 100644 index 000000000..b9b2b3b5d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_python_info + - setup_remote_constraints + - setup_pkg_mgr diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/tasks/main.yml new file mode 100644 index 000000000..cd5a5260b --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/tasks/main.yml @@ -0,0 +1,71 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Install from system packages + when: ansible_os_family != "Darwin" and target_system_python + block: + + - name: Include OS-specific variables + include_vars: '{{ lookup("first_found", search) }}' + vars: + search: + files: + - '{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml' + - '{{ ansible_distribution }}-{{ ansible_distribution_version }}.yml' + - '{{ ansible_distribution }}.yml' + - '{{ ansible_os_family }}.yml' + paths: + - vars + + - when: has_pyopenssl + block: + + - name: Install pyOpenSSL (Python 3 from system packages) + become: true + package: + name: '{{ pyopenssl_package_name_python3 }}' + when: ansible_python_version is version('3.0', '>=') + + - name: Install pyOpenSSL (Python 2 from system packages) + become: true + package: + name: '{{ pyopenssl_package_name }}' + when: ansible_python_version is version('3.0', '<') + +- name: Install from PyPi + when: ansible_os_family == "Darwin" or not target_system_python + block: + + - name: Install pyOpenSSL (PyPi) + become: true + pip: + name: pyOpenSSL + state: "{{ 'latest' if not target_system_python_cannot_upgrade_cryptography else omit }}" + extra_args: "-c {{ remote_constraints }}" + +- when: has_pyopenssl + block: + + - name: Register pyOpenSSL version + command: "{{ ansible_python.executable }} -c 'import OpenSSL; print(OpenSSL.__version__)'" + register: pyopenssl_version + + - name: Register pyOpenSSL debug details + command: "{{ ansible_python.executable }} -m OpenSSL.debug" + register: pyopenssl_debug_version + ignore_errors: true + +# Depending on which pyOpenSSL version has been installed, it could be that cryptography has +# been upgraded to a newer version. Make sure to register cryptography_version another time here +# to avoid strange testing behavior due to wrong values of cryptography_version. +- name: Register cryptography version + command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'" + register: cryptography_version + ignore_errors: true # in case cryptography was not installed, and setup_openssl hasn't been run before, ignore errors diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/Alpine.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/Alpine.yml new file mode 100644 index 000000000..e0aa36588 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/Alpine.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +pyopenssl_package_name: py-openssl +pyopenssl_package_name_python3: py3-openssl diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/Archlinux.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/Archlinux.yml new file mode 100644 index 000000000..08ca08f10 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/Archlinux.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +pyopenssl_package_name: python-pyopenssl +pyopenssl_package_name_python3: python-pyopenssl diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/Debian.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/Debian.yml new file mode 100644 index 000000000..85c86de25 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/Debian.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +pyopenssl_package_name: python-openssl +pyopenssl_package_name_python3: python3-openssl diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/FreeBSD.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/FreeBSD.yml new file mode 100644 index 000000000..6d4cbbdb8 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/FreeBSD.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +pyopenssl_package_name: py27-openssl +pyopenssl_package_name_python3: "py{{ ansible_python.version.major }}{{ ansible_python.version.minor }}-openssl" diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/RedHat-9.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/RedHat-9.yml new file mode 100644 index 000000000..4de0ee222 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/RedHat-9.yml @@ -0,0 +1,6 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +has_pyopenssl: false diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/RedHat.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/RedHat.yml new file mode 100644 index 000000000..aaeea70fb --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/RedHat.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +pyopenssl_package_name: pyOpenSSL +pyopenssl_package_name_python3: python3-pyOpenSSL diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/Suse.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/Suse.yml new file mode 100644 index 000000000..4bdfa3226 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_pyopenssl/vars/Suse.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +pyopenssl_package_name: python-pyOpenSSL +pyopenssl_package_name_python3: python3-pyOpenSSL diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_python_info/filter_plugins/version_filter.py b/ansible_collections/community/crypto/tests/integration/targets/setup_python_info/filter_plugins/version_filter.py new file mode 100644 index 000000000..2dc985168 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_python_info/filter_plugins/version_filter.py @@ -0,0 +1,41 @@ +# Copyright (c) 2021, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +def get_major_minor_version(version): + parts = version.split('.')[:2] + return '.'.join(parts) + + +def version_lookup(data, distribution, os_family, distribution_version, distribution_major_version, python_version, default_value=False): + if distribution in data: + data = data[distribution] + elif os_family in data: + data = data[os_family] + else: + return default_value + + if distribution_version in data: + data = data[distribution_version] + elif get_major_minor_version(distribution_version) in data: + data = data[get_major_minor_version(distribution_version)] + elif str(distribution_major_version) in data: + data = data[str(distribution_major_version)] + else: + return default_value + + return python_version in data + + +class FilterModule(object): + """ IP address and network manipulation filters """ + + def filters(self): + return { + 'internal__get_major_minor_version': get_major_minor_version, + 'internal__version_lookup': version_lookup, + } diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_python_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_python_info/tasks/main.yml new file mode 100644 index 000000000..1b539515f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_python_info/tasks/main.yml @@ -0,0 +1,73 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Gather facts on controller + setup: + gather_subset: '!all' + delegate_to: localhost + delegate_facts: true + run_once: true +- name: Show variables + debug: + msg: |- + Target: + Python: {{ ansible_facts.python.version.major ~ '.' ~ ansible_facts.python.version.minor }} + OS family: {{ ansible_facts.os_family }} + Distribution: {{ ansible_facts.distribution }} + Distribution version: {{ ansible_facts.distribution_version | internal__get_major_minor_version }} + Distribution major version: {{ ansible_facts.distribution_major_version }} + + Controller: + Python: {{ hostvars['localhost'].ansible_facts.python.version.major ~ '.' ~ hostvars['localhost'].ansible_facts.python.version.minor }} + OS family: {{ hostvars['localhost'].ansible_facts.os_family }} + Distribution: {{ hostvars['localhost'].ansible_facts.distribution }} + Distribution version: {{ hostvars['localhost'].ansible_facts.distribution_version | internal__get_major_minor_version }} + Distribution major version: {{ hostvars['localhost'].ansible_facts.distribution_major_version }} +- name: Record information + set_fact: + target_system_python: >- + {{ + system_python_version_data | + internal__version_lookup( + ansible_facts.distribution, + ansible_facts.os_family, + ansible_facts.distribution_version, + ansible_facts.distribution_major_version, + ansible_facts.python.version.major ~ '.' ~ ansible_facts.python.version.minor + ) + }} + target_system_python_cannot_upgrade_cryptography: >- + {{ + cannot_upgrade_cryptography | + internal__version_lookup( + ansible_facts.distribution, + ansible_facts.os_family, + ansible_facts.distribution_version, + ansible_facts.distribution_major_version, + ansible_facts.python.version.major ~ '.' ~ ansible_facts.python.version.minor + ) + }} + controller_system_python: >- + {{ + system_python_version_data | + internal__version_lookup( + hostvars['localhost'].ansible_facts.distribution, + hostvars['localhost'].ansible_facts.os_family, + hostvars['localhost'].ansible_facts.distribution_version, + hostvars['localhost'].ansible_facts.distribution_major_version, + hostvars['localhost'].ansible_facts.python.version.major ~ '.' ~ hostvars['localhost'].ansible_facts.python.version.minor + ) + }} + controller_system_python_cannot_upgrade_cryptography: >- + {{ + cannot_upgrade_cryptography | + internal__version_lookup( + hostvars['localhost'].ansible_facts.distribution, + hostvars['localhost'].ansible_facts.os_family, + hostvars['localhost'].ansible_facts.distribution_version, + hostvars['localhost'].ansible_facts.distribution_major_version, + hostvars['localhost'].ansible_facts.python.version.major ~ '.' ~ hostvars['localhost'].ansible_facts.python.version.minor + ) + }} diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_python_info/vars/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_python_info/vars/main.yml new file mode 100644 index 000000000..ec2170aed --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_python_info/vars/main.yml @@ -0,0 +1,91 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +system_python_version_data: + CentOS: + '6': + - '2.6' + '7': + - '2.7' + '8': + - '3.6' + Fedora: + '30': + - '3.7' + '31': + - '3.7' + '32': + - '3.8' + '33': + - '3.9' + '34': + - '3.9' + '35': + - '3.10' + '36': + - '3.10' + Ubuntu: + '16': + - '2.7' + '18': + - '3.6' + '20': + - '3.8' + '22': + - '3.10' + Darwin: + '10.11': + - '2.7' + '10.15': + - '3.8' + '11.1': + - '3.9' + '12.0': + - '3.10' + FreeBSD: + '12.1': + - '3.6' + '12.2': + - '3.7' + '12.3': + - '3.8' + '13.0': + - '3.7' + '13.1': + - '3.8' + RedHat: + '7': + - '2.7' + '8': + - '3.6' + '9.0': + - '3.9' + Suse: + '15': + - '2.7' + - '3.6' + Archlinux: + 'NA': + - '3.10' + Debian: + '11': + - '3.9' + Alpine: + '3.16': + - '3.10' + '3.15': + - '3.9' + '3.12': + - '3.8' + +cannot_upgrade_cryptography: + FreeBSD: + '12.2': + - '3.8' # on the VMs in CI, system packages are used for this version as well + '13.0': + - '3.8' # on the VMs in CI, system packages are used for this version as well + Ubuntu: + '18': + - '3.9' # this is the default container for ansible-core 2.12; upgrading cryptography wrecks pyOpenSSL diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_remote_constraints/aliases b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_constraints/aliases new file mode 100644 index 000000000..27ce6b087 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_constraints/aliases @@ -0,0 +1,5 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +needs/file/tests/utils/constraints.txt diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_remote_constraints/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_constraints/meta/main.yml new file mode 100644 index 000000000..982de6eb0 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_constraints/meta/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_remote_constraints/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_constraints/tasks/main.yml new file mode 100644 index 000000000..7e913fc91 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_constraints/tasks/main.yml @@ -0,0 +1,18 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: record constraints.txt path on remote host + set_fact: + remote_constraints: "{{ remote_tmp_dir }}/constraints.txt" + +- name: copy constraints.txt to remote host + copy: + src: "{{ role_path }}/../../../utils/constraints.txt" + dest: "{{ remote_constraints }}" diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml new file mode 100644 index 000000000..237db0fac --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: delete temporary directory + include_tasks: default-cleanup.yml diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml new file mode 100644 index 000000000..cc74b70af --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml @@ -0,0 +1,10 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: delete temporary directory + file: + path: "{{ remote_tmp_dir }}" + state: absent + no_log: true diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_remote_tmp_dir/tasks/default.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_tmp_dir/tasks/default.yml new file mode 100644 index 000000000..95c513194 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_tmp_dir/tasks/default.yml @@ -0,0 +1,22 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: create ~/tmp + file: + path: '~/tmp' + state: directory + +- name: create temporary directory + tempfile: + state: directory + suffix: .test + path: '~/tmp' + register: remote_tmp_dir + notify: + - delete temporary directory + +- name: record temporary directory + set_fact: + remote_tmp_dir: "{{ remote_tmp_dir.path }}" diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml new file mode 100644 index 000000000..babbdad05 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml @@ -0,0 +1,20 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: make sure we have the ansible_os_family and ansible_distribution_version facts + setup: + gather_subset: distribution + when: ansible_facts == {} + +- include_tasks: "{{ lookup('first_found', files)}}" + vars: + files: + - "{{ ansible_os_family | lower }}.yml" + - "default.yml" diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_agent/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_agent/meta/main.yml new file mode 100644 index 000000000..231aee9de --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_agent/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_ssh_keygen + - prepare_jinja2_compat diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_agent/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_agent/tasks/main.yml new file mode 100644 index 000000000..2e224fb8f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_agent/tasks/main.yml @@ -0,0 +1,56 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Start an ssh agent to use for tests + shell: ssh-agent -c | grep "^setenv" + register: openssh_agent_stdout + +- name: Convert output to dictionary + set_fact: + openssh_agent_env: >- + {{ + openssh_agent_stdout.stdout_lines | map('regex_replace', '^setenv ([^ ]+) ([^ ]+);', '\1') + | zip(openssh_agent_stdout.stdout_lines | map('regex_replace', '^setenv ([^ ]+) ([^ ]+);', '\2')) + | list | items2dict(key_name=0, value_name=1) + }} + +- name: Register ssh agent facts + set_fact: + openssh_agent_pid: "{{ openssh_agent_env.SSH_AGENT_PID }}" + openssh_agent_sock: "{{ openssh_agent_env.SSH_AUTH_SOCK }}" + +- name: stat agent socket + stat: + path: "{{ openssh_agent_sock }}" + register: openssh_agent_socket_stat + +- name: Assert agent socket file is a socket + assert: + that: + - openssh_agent_socket_stat.stat.issock is defined + - openssh_agent_socket_stat.stat.issock + fail_msg: "{{ openssh_agent_sock }} is not a socket" + +- name: Verify agent responds + command: ssh-add -l + register: rc_openssh_agent_ssh_add_check + environment: + SSH_AUTH_SOCK: "{{ openssh_agent_sock }}" + when: openssh_agent_socket_stat.stat.issock + failed_when: rc_openssh_agent_ssh_add_check.rc == 2 + +- name: Get ssh version + shell: ssh -Vq 2>&1|sed 's/^.*OpenSSH_\([0-9]\{1,\}\.[0-9]\{1,\}\).*$/\1/' + register: + rc_openssh_version_output + +- name: Set ssh version facts + set_fact: + openssh_version: "{{ rc_openssh_version_output.stdout.strip() }}" diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/meta/main.yml new file mode 100644 index 000000000..2fcd152f9 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/meta/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_pkg_mgr diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/tasks/main.yml new file mode 100644 index 000000000..22574431d --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/tasks/main.yml @@ -0,0 +1,27 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Include OS-specific variables + include_vars: '{{ ansible_os_family }}.yml' + when: not ansible_os_family == "Darwin" and not ansible_os_family == "FreeBSD" + +- name: Install ssh-keygen + package: + name: '{{ openssh_client_package_name }}' + when: not ansible_os_family == "Darwin" and not ansible_os_family == "FreeBSD" + +- name: Get ssh version + shell: ssh -Vq 2>&1|sed 's/^.*OpenSSH_\([0-9]\{1,\}\.[0-9]\{1,\}\).*$/\1/' + register: + rc_openssh_version_output + +- name: Set ssh version facts + set_fact: + openssh_version: "{{ rc_openssh_version_output.stdout.strip() }}" diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/Alpine.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/Alpine.yml new file mode 100644 index 000000000..7efacc4ca --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/Alpine.yml @@ -0,0 +1,6 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +openssh_client_package_name: openssh diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/Archlinux.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/Archlinux.yml new file mode 100644 index 000000000..7efacc4ca --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/Archlinux.yml @@ -0,0 +1,6 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +openssh_client_package_name: openssh diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/Debian.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/Debian.yml new file mode 100644 index 000000000..f1c64514b --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/Debian.yml @@ -0,0 +1,6 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +openssh_client_package_name: openssh-client diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/RedHat.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/RedHat.yml new file mode 100644 index 000000000..29bcb8574 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/RedHat.yml @@ -0,0 +1,6 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +openssh_client_package_name: openssh-clients diff --git a/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/Suse.yml b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/Suse.yml new file mode 100644 index 000000000..7efacc4ca --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/setup_ssh_keygen/vars/Suse.yml @@ -0,0 +1,6 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +openssh_client_package_name: openssh diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate-acme/aliases b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate-acme/aliases new file mode 100644 index 000000000..9b02df38c --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate-acme/aliases @@ -0,0 +1,11 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/1 +azp/posix/1 +cloud/acme +context/target + +# For some reason connecting to helper containers does not work on the Alpine VMs +skip/alpine diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate-acme/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate-acme/meta/main.yml new file mode 100644 index 000000000..d71644584 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate-acme/meta/main.yml @@ -0,0 +1,10 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_acme + - setup_pyopenssl # needed for Ubuntu 16.04 + - setup_remote_tmp_dir + - prepare_jinja2_compat diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate-acme/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate-acme/tasks/impl.yml new file mode 100644 index 000000000..08e113d2b --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate-acme/tasks/impl.yml @@ -0,0 +1,73 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Generate account key + openssl_privatekey: + path: '{{ remote_tmp_dir }}/account.key' + size: '{{ default_rsa_key_size }}' + +- name: Generate privatekey + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey.pem' + size: '{{ default_rsa_key_size }}' + +- name: Generate CSRs + openssl_csr: + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + path: '{{ remote_tmp_dir }}/{{ item.name }}.csr' + subject_alt_name: '{{ item.sans }}' + loop: + - name: cert-1 + sans: + - DNS:example.com + - name: cert-2 + sans: + - DNS:example.com + - DNS:example.org + +- name: Retrieve certificate 1 + x509_certificate: + provider: acme + path: '{{ remote_tmp_dir }}/cert-1.pem' + csr_path: '{{ remote_tmp_dir }}/cert-1.csr' + acme_accountkey_path: '{{ remote_tmp_dir }}/account.key' + acme_challenge_path: '{{ remote_tmp_dir }}/challenges/' + acme_directory: https://{{ acme_host }}:14000/dir + environment: + PATH: '{{ lookup("env", "PATH") }}:{{ remote_tmp_dir }}' + +- name: Get certificate information + x509_certificate_info: + path: '{{ remote_tmp_dir }}/cert-1.pem' + register: result + +- name: Validate certificate information + assert: + that: + - result.subject_alt_name | length == 1 + - "'DNS:example.com' in result.subject_alt_name" + +- name: Retrieve certificate 2 + x509_certificate: + provider: acme + path: '{{ remote_tmp_dir }}/cert-2.pem' + csr_path: '{{ remote_tmp_dir }}/cert-2.csr' + acme_accountkey_path: '{{ remote_tmp_dir }}/account.key' + acme_challenge_path: '{{ remote_tmp_dir }}/challenges/' + acme_directory: https://{{ acme_host }}:14000/dir + environment: + PATH: '{{ lookup("env", "PATH") }}:{{ remote_tmp_dir }}' + +- name: Get certificate information + x509_certificate_info: + path: '{{ remote_tmp_dir }}/cert-2.pem' + register: result + +- name: Validate certificate information + assert: + that: + - result.subject_alt_name | length == 2 + - "'DNS:example.com' in result.subject_alt_name" + - "'DNS:example.org' in result.subject_alt_name" diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate-acme/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate-acme/tasks/main.yml new file mode 100644 index 000000000..e8f2fff89 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate-acme/tasks/main.yml @@ -0,0 +1,144 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- block: + - name: Obtain root and intermediate certificates + get_url: + url: "http://{{ acme_host }}:5000/{{ item.0 }}-certificate-for-ca/{{ item.1 }}" + dest: "{{ remote_tmp_dir }}/acme-{{ item.0 }}-{{ item.1 }}.pem" + loop: "{{ query('nested', types, root_numbers) }}" + + - name: Analyze root certificates + x509_certificate_info: + path: "{{ remote_tmp_dir }}/acme-root-{{ item }}.pem" + loop: "{{ root_numbers }}" + register: acme_roots + + - name: Analyze intermediate certificates + x509_certificate_info: + path: "{{ remote_tmp_dir }}/acme-intermediate-{{ item }}.pem" + loop: "{{ root_numbers }}" + register: acme_intermediates + + - name: Read root certificates + slurp: + src: "{{ remote_tmp_dir ~ '/acme-root-' ~ item ~ '.pem' }}" + loop: "{{ root_numbers }}" + register: slurp_roots + + - set_fact: + x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}" + loop: "{{ acme_roots.results }}" + register: acme_roots_tmp + + - name: Read intermediate certificates + slurp: + src: "{{ remote_tmp_dir ~ '/acme-intermediate-' ~ item ~ '.pem' }}" + loop: "{{ root_numbers }}" + register: slurp_intermediates + + - set_fact: + x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}" + 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: "{{ slurp_roots.results | map(attribute='content') | map('b64decode') | list }}" + acme_intermediates: "{{ acme_intermediates_tmp.results | map(attribute='ansible_facts.x__') | list }}" + acme_intermediate_certs: "{{ slurp_intermediates.results | map(attribute='content') | map('b64decode') | list }}" + + vars: + types: + - root + - intermediate + root_numbers: + - 0 + interesting_keys: + - authority_key_identifier + - subject_key_identifier + - issuer + - subject + +- name: Get hold of acme-tiny executable + get_url: + url: https://raw.githubusercontent.com/diafygi/acme-tiny/master/acme_tiny.py + dest: "{{ remote_tmp_dir }}/acme-tiny" + when: ansible_python_version is version('2.7', '>=') + +- name: Get hold of acme-tiny executable (Python 2.6) + command: + cmd: >- + curl https://raw.githubusercontent.com/diafygi/acme-tiny/master/acme_tiny.py --output "{{ remote_tmp_dir }}/acme-tiny" + when: ansible_python_version is version('2.7', '<') + +- name: Make sure acme-tiny is executable + file: + path: "{{ remote_tmp_dir }}/acme-tiny" + mode: "0755" + +- name: "Monkey-patch acme-tiny: Disable certificate validation" + blockinfile: + path: "{{ remote_tmp_dir }}/acme-tiny" + marker: "# {mark} ANSIBLE MANAGED BLOCK: DISABLE CERTIFICATE VALIDATION FOR HTTPS REQUESTS" + insertafter: '^#!.*' + block: | + import ssl + try: + ssl._create_default_https_context = ssl._create_unverified_context + except Exception: + # Python before 2.7.9 has no verification at all. So nothing to disable. + pass + # For later: + try: + from urllib.request import Request # Python 3 + except ImportError: + from urllib2 import Request # Python 2 + +- name: "Monkey-patch acme-tiny: adjust shebang" + replace: + path: "{{ remote_tmp_dir }}/acme-tiny" + regexp: '^\#\!/usr/bin/env .*$' + replace: '#!{{ ansible_python_interpreter }}' + +- name: "Monkey-patch acme-tiny: Disable check that challenge file is reachable via HTTP" + replace: + path: "{{ remote_tmp_dir }}/acme-tiny" + regexp: 'parser\.add_argument\("--disable-check", default=False,' + replace: 'parser.add_argument("--disable-check", default=True,' + +- name: "Monkey-patch acme-tiny: Instead of writing challenge files to disk, post them to challenge server" + replace: + path: "{{ remote_tmp_dir }}/acme-tiny" + regexp: 'with open\(wellknown_path, "w"\) as [^:]+:\n\s+[^. ]+\.write\(([^)]+)\)' + replace: 'r = Request(url="http://{{ acme_host }}:5000/http/" + domain + "/" + token, data=\1.encode("utf8"), headers={"content-type": "application/octet-stream"}) ; r.get_method = lambda: "PUT" ; urlopen(r).close()' + +- name: "Monkey-patch acme-tiny: Remove file cleanup" + replace: + path: "{{ remote_tmp_dir }}/acme-tiny" + regexp: 'os\.remove\(wellknown_path\)' + replace: 'pass' + +- name: "Monkey-patch acme-tiny: Allow to run with Python 2" + replace: + path: "{{ remote_tmp_dir }}/acme-tiny" + regexp: '#!/usr/bin/env python3' + replace: '#!/usr/bin/env python' + when: ansible_facts.python.version.major == 2 + +- name: Create challenges directory + file: + path: '{{ remote_tmp_dir }}/challenges' + state: directory + +- name: Running tests + include_tasks: impl.yml + # Make x509_certificate module happy + when: cryptography_version.stdout is version('1.6', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/aliases b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/impl.yml new file mode 100644 index 000000000..593de0502 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/impl.yml @@ -0,0 +1,10 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- debug: + msg: "Executing tests with backend {{ select_crypto_backend }}" +- import_tasks: selfsigned.yml +- import_tasks: ownca.yml +- import_tasks: removal.yml diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/main.yml new file mode 100644 index 000000000..3253f3968 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/main.yml @@ -0,0 +1,15 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + when: cryptography_version.stdout is version('1.6', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/ownca.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/ownca.yml new file mode 100644 index 000000000..99832a517 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/ownca.yml @@ -0,0 +1,651 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: (OwnCA, {{select_crypto_backend}}) Generate CA privatekey + openssl_privatekey: + path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + size: '{{ default_rsa_key_size_certifiates }}' + +- name: (OwnCA, {{select_crypto_backend}}) Generate CA privatekey with passphrase + openssl_privatekey: + path: '{{ remote_tmp_dir }}/ca_privatekey_pw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + size: '{{ default_rsa_key_size_certifiates }}' + +- name: (OwnCA, {{select_crypto_backend}}) Generate CA CSR + openssl_csr: + path: '{{ item.path }}' + privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + subject: '{{ item.subject }}' + useCommonNameForSAN: false + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: true + loop: + - path: '{{ remote_tmp_dir }}/ca_csr.csr' + subject: + commonName: Example CA + - path: '{{ remote_tmp_dir }}/ca_csr2.csr' + subject: + commonName: Example CA 2 + +- name: (OwnCA, {{select_crypto_backend}}) Generate CA CSR (privatekey passphrase) + openssl_csr: + path: '{{ remote_tmp_dir }}/ca_csr_pw.csr' + privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey_pw.pem' + privatekey_passphrase: hunter2 + subject: + commonName: Example CA + useCommonNameForSAN: false + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: true + +- name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned CA certificate (check mode) + x509_certificate: + path: '{{ remote_tmp_dir }}/ca_cert.pem' + csr_path: '{{ remote_tmp_dir }}/ca_csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: result_check_mode + +- name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned CA certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/ca_cert.pem' + csr_path: '{{ remote_tmp_dir }}/ca_csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: (OwnCA, {{select_crypto_backend}}) Verify changed + assert: + that: + - result_check_mode is changed + - result is changed + +- name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned CA certificate with different commonName + x509_certificate: + path: '{{ remote_tmp_dir }}/ca_cert2.pem' + csr_path: '{{ remote_tmp_dir }}/ca_csr2.csr' + privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned CA certificate (privatekey passphrase) + x509_certificate: + path: '{{ remote_tmp_dir }}/ca_cert_pw.pem' + csr_path: '{{ remote_tmp_dir }}/ca_csr_pw.csr' + privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey_pw.pem' + privatekey_passphrase: hunter2 + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: ownca_certificate + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (idempotent) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: ownca_certificate_idempotence + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (check mode) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + +- name: (OwnCA, {{select_crypto_backend}}) Copy ownca certificate to new file to check regeneration + copy: + src: '{{ remote_tmp_dir }}/ownca_cert.pem' + dest: '{{ item }}' + remote_src: true + loop: + - '{{ remote_tmp_dir }}/ownca_cert_ca_cn.pem' + - '{{ remote_tmp_dir }}/ownca_cert_ca_key.pem' + +- name: (OwnCA, {{select_crypto_backend}}) Regenerate ownca certificate with different CA subject + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_ca_cn.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_path: '{{ remote_tmp_dir }}/ca_cert2.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: ownca_certificate_ca_subject_changed + +- name: (OwnCA, {{select_crypto_backend}}) Regenerate ownca certificate with different CA key + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_ca_key.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_path: '{{ remote_tmp_dir }}/ca_cert_pw.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey_pw.pem' + ownca_privatekey_passphrase: hunter2 + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: ownca_certificate_ca_key_changed + +- name: (OwnCA, {{select_crypto_backend}}) Get certificate information + community.crypto.x509_certificate_info: + path: '{{ remote_tmp_dir }}/ownca_cert.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: (OwnCA, {{select_crypto_backend}}) Get private key information + community.crypto.openssl_privatekey_info: + path: '{{ remote_tmp_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_privatekey + +- name: (OwnCA, {{select_crypto_backend}}) Check ownca certificate + assert: + that: + - result.public_key == result_privatekey.public_key + - "result.signature_algorithm == 'sha256WithRSAEncryption' or result.signature_algorithm == 'sha256WithECDSAEncryption'" + - "result.subject.commonName == 'www.example.com'" + - "result.issuer.commonName == 'Example CA'" + - not result.expired + - result.version == 3 + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca v2 certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_v2.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_version: 2 + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_v2_certificate + ignore_errors: true + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate2 + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert2.pem' + csr_path: '{{ remote_tmp_dir }}/csr2.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey2.pem' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (OwnCA, {{select_crypto_backend}}) Get certificate information + community.crypto.x509_certificate_info: + path: '{{ remote_tmp_dir }}/ownca_cert2.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: (OwnCA, {{select_crypto_backend}}) Get private key information + community.crypto.openssl_privatekey_info: + path: '{{ remote_tmp_dir }}/privatekey2.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_privatekey + +- name: (OwnCA, {{select_crypto_backend}}) Check ownca certificate2 + assert: + that: + - result.public_key == result_privatekey.public_key + - "result.signature_algorithm == 'sha256WithRSAEncryption' or result.signature_algorithm == 'sha256WithECDSAEncryption'" + - "result.subject.commonName == 'www.example.com'" + - "result.subject.countryName == 'US'" + - "result.subject.localityName == 'Los Angeles'" # L + - "result.subject.organizationName == 'ACME Inc.'" + - "['organizationalUnitName', 'Pyrotechnics'] in result.subject_ordered" + - "['organizationalUnitName', 'Roadrunner pest control'] in result.subject_ordered" + - "result.issuer.commonName == 'Example CA'" + - not result.expired + - result.version == 3 + - "'Digital Signature' in result.key_usage" + - "'IPSec User' in result.extended_key_usage" + - "'Biometric Info' in result.extended_key_usage" + +- name: (OwnCA, {{select_crypto_backend}}) Create ownca certificate with notBefore and notAfter + x509_certificate: + provider: ownca + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + path: "{{ remote_tmp_dir }}/ownca_cert3.pem" + csr_path: "{{ remote_tmp_dir }}/csr.csr" + privatekey_path: "{{ remote_tmp_dir }}/privatekey3.pem" + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (OwnCA, {{select_crypto_backend}}) Create ownca certificate with relative notBefore and notAfter + x509_certificate: + provider: ownca + ownca_not_before: +1s + ownca_not_after: +52w + path: "{{ remote_tmp_dir }}/ownca_cert4.pem" + csr_path: "{{ remote_tmp_dir }}/csr.csr" + privatekey_path: "{{ remote_tmp_dir }}/privatekey3.pem" + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca ECC certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_ecc.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_certificate_ecc + +- name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned certificate (privatekey passphrase) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_ecc_2.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert_pw.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey_pw.pem' + ownca_privatekey_passphrase: hunter2 + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_certificate_passphrase + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (failed passphrase 1) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_pw1.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + ownca_privatekey_passphrase: hunter2 + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: passphrase_error_1 + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (failed passphrase 2) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_pw2.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + ownca_privatekey_passphrase: wrong_password + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: passphrase_error_2 + +- name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (failed passphrase 3) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_pw3.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: passphrase_error_3 + +- name: (OwnCA, {{select_crypto_backend}}) Create broken certificate + copy: + dest: "{{ remote_tmp_dir }}/ownca_broken.pem" + content: "broken" +- name: (OwnCA, {{select_crypto_backend}}) Regenerate broken cert + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_broken.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + register: ownca_broken + +- name: (OwnCA, {{select_crypto_backend}}) Backup test + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_backup_1 +- name: (OwnCA, {{select_crypto_backend}}) Backup test (idempotent) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_backup_2 +- name: (OwnCA, {{select_crypto_backend}}) Backup test (change) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_backup_3 +- name: (OwnCA, {{select_crypto_backend}}) Backup test (remove) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem' + state: absent + provider: ownca + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_backup_4 +- name: (OwnCA, {{select_crypto_backend}}) Backup test (remove, idempotent) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem' + state: absent + provider: ownca + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_backup_5 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_subject_key_identifier_1 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (idempotency) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_subject_key_identifier_2 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (remove) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: never_create + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_subject_key_identifier_3 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (remove idempotency) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: never_create + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_subject_key_identifier_4 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (re-enable) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_subject_key_identifier_5 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_authority_key_identifier_1 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (idempotency) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_authority_key_identifier_2 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (remove) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: false + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_authority_key_identifier_3 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (remove idempotency) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: false + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_authority_key_identifier_4 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (re-add) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_authority_key_identifier_5 + +- name: (OwnCA, {{select_crypto_backend}}) Ed25519 and Ed448 tests (for cryptography >= 2.6) + block: + - name: (OwnCA, {{select_crypto_backend}}) Generate privatekeys + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_{{ item }}.pem' + type: '{{ item }}' + loop: + - Ed25519 + - Ed448 + register: ownca_certificate_ed25519_ed448_privatekey + ignore_errors: true + + - name: (OwnCA, {{select_crypto_backend}}) Generate CSR etc. if private key generation succeeded + when: ownca_certificate_ed25519_ed448_privatekey is not failed + block: + + - name: (OwnCA, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_{{ item }}.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + ignore_errors: true + + - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_{{ item }}.pem' + csr_path: '{{ remote_tmp_dir }}/csr_{{ item }}.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: ownca_certificate_ed25519_ed448 + ignore_errors: true + + - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (idempotent) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_{{ item }}.pem' + csr_path: '{{ remote_tmp_dir }}/csr_{{ item }}.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: ownca_certificate_ed25519_ed448_idempotence + ignore_errors: true + + - name: (OwnCA, {{select_crypto_backend}}) Generate CA privatekey + openssl_privatekey: + path: '{{ remote_tmp_dir }}/ca_privatekey_{{ item }}.pem' + type: '{{ item }}' + cipher: auto + passphrase: Test123 + ignore_errors: true + loop: + - Ed25519 + - Ed448 + + - name: (OwnCA, {{select_crypto_backend}}) Generate CA CSR + openssl_csr: + path: '{{ remote_tmp_dir }}/ca_csr_{{ item }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey_{{ item }}.pem' + privatekey_passphrase: Test123 + subject: + commonName: Example CA + useCommonNameForSAN: false + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: true + key_usage: + - cRLSign + - keyCertSign + loop: + - Ed25519 + - Ed448 + ignore_errors: true + + - name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned CA certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/ca_cert_{{ item }}.pem' + csr_path: '{{ remote_tmp_dir }}/ca_csr_{{ item }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey_{{ item }}.pem' + privatekey_passphrase: Test123 + provider: selfsigned + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + ignore_errors: true + + - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_{{ item }}_2.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert_{{ item }}.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey_{{ item }}.pem' + ownca_privatekey_passphrase: Test123 + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: ownca_certificate_ed25519_ed448_2 + ignore_errors: true + + - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (idempotent) + x509_certificate: + path: '{{ remote_tmp_dir }}/ownca_cert_{{ item }}_2.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + ownca_path: '{{ remote_tmp_dir }}/ca_cert_{{ item }}.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey_{{ item }}.pem' + ownca_privatekey_passphrase: Test123 + provider: ownca + ownca_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: ownca_certificate_ed25519_ed448_2_idempotence + ignore_errors: true + + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.6', '>=') + +- import_tasks: ../tests/validate_ownca.yml diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/removal.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/removal.yml new file mode 100644 index 000000000..c79c527a8 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/removal.yml @@ -0,0 +1,57 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: (Removal, {{select_crypto_backend}}) Generate privatekey + openssl_privatekey: + path: '{{ remote_tmp_dir }}/removal_privatekey.pem' + size: '{{ default_rsa_key_size_certifiates }}' + +- name: (Removal, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ remote_tmp_dir }}/removal_csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/removal_privatekey.pem' + +- name: (Removal, {{select_crypto_backend}}) Generate selfsigned certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/removal_cert.pem' + csr_path: '{{ remote_tmp_dir }}/removal_csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/removal_privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: "(Removal, {{select_crypto_backend}}) Check that file is not gone" + stat: + path: "{{ remote_tmp_dir }}/removal_cert.pem" + register: removal_1_prestat + +- name: "(Removal, {{select_crypto_backend}}) Remove certificate" + x509_certificate: + path: "{{ remote_tmp_dir }}/removal_cert.pem" + state: absent + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: removal_1 + +- name: "(Removal, {{select_crypto_backend}}) Check that file is gone" + stat: + path: "{{ remote_tmp_dir }}/removal_cert.pem" + register: removal_1_poststat + +- name: "(Removal, {{select_crypto_backend}}) Remove certificate (idempotent)" + x509_certificate: + path: "{{ remote_tmp_dir }}/removal_cert.pem" + state: absent + select_crypto_backend: '{{ select_crypto_backend }}' + register: removal_2 + +- name: (Removal, {{select_crypto_backend}}) Ensure removal worked + assert: + that: + - removal_1_prestat.stat.exists + - removal_1 is changed + - not removal_1_poststat.stat.exists + - removal_2 is not changed + - removal_1.certificate is none diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/selfsigned.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/selfsigned.yml new file mode 100644 index 000000000..a0f23643b --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/selfsigned.yml @@ -0,0 +1,474 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: (Selfsigned, {{select_crypto_backend}}) Generate privatekey + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey.pem' + size: '{{ default_rsa_key_size_certifiates }}' + +- name: (Selfsigned, {{select_crypto_backend}}) Generate privatekey with password + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + size: '{{ default_rsa_key_size_certifiates }}' + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate without CSR + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_no_csr.pem' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: selfsigned_certificate_no_csr + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate without CSR - idempotency + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_no_csr.pem' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: selfsigned_certificate_no_csr_idempotence + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate without CSR (check mode) + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_no_csr.pem' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: selfsigned_certificate_no_csr_idempotence_check + +- name: (Selfsigned, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.example.com + +- name: (Selfsigned, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_minimal_change.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.example.org + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/cert.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: selfsigned_certificate + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate - idempotency + x509_certificate: + path: '{{ remote_tmp_dir }}/cert.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: true + register: selfsigned_certificate_idempotence + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (check mode) + x509_certificate: + path: '{{ remote_tmp_dir }}/cert.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (check mode, other CSR) + x509_certificate: + path: '{{ remote_tmp_dir }}/cert.pem' + csr_path: '{{ remote_tmp_dir }}/csr_minimal_change.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: selfsigned_certificate_csr_minimal_change + +- name: (Selfsigned, {{select_crypto_backend}}) Get certificate information + community.crypto.x509_certificate_info: + path: '{{ remote_tmp_dir }}/cert.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: (Selfsigned, {{select_crypto_backend}}) Get private key information + community.crypto.openssl_privatekey_info: + path: '{{ remote_tmp_dir }}/privatekey.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_privatekey + +- name: (Selfsigned, {{select_crypto_backend}}) Check selfsigned certificate + assert: + that: + - result.public_key == result_privatekey.public_key + - "result.signature_algorithm == 'sha256WithRSAEncryption' or result.signature_algorithm == 'sha256WithECDSAEncryption'" + - "result.subject.commonName == 'www.example.com'" + - not result.expired + - result.version == 3 + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned v2 certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_v2.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_version: 2 + select_crypto_backend: "{{ select_crypto_backend }}" + register: selfsigned_v2_cert + ignore_errors: true + +- name: (Selfsigned, {{select_crypto_backend}}) Generate privatekey2 + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey2.pem' + size: '{{ default_rsa_key_size_certifiates }}' + +- name: (Selfsigned, {{select_crypto_backend}}) Generate CSR2 + openssl_csr: + subject: + CN: www.example.com + C: US + ST: California + L: Los Angeles + O: ACME Inc. + OU: + - Roadrunner pest control + - Pyrotechnics + path: '{{ remote_tmp_dir }}/csr2.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey2.pem' + keyUsage: + - digitalSignature + extendedKeyUsage: + - ipsecUser + - biometricInfo + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate2 + x509_certificate: + path: '{{ remote_tmp_dir }}/cert2.pem' + csr_path: '{{ remote_tmp_dir }}/csr2.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey2.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (Selfsigned, {{select_crypto_backend}}) Get certificate information + community.crypto.x509_certificate_info: + path: '{{ remote_tmp_dir }}/cert2.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: (Selfsigned, {{select_crypto_backend}}) Get private key information + community.crypto.openssl_privatekey_info: + path: '{{ remote_tmp_dir }}/privatekey2.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_privatekey + +- name: (Selfsigned, {{select_crypto_backend}}) Check selfsigned certificate2 + assert: + that: + - result.public_key == result_privatekey.public_key + - "result.signature_algorithm == 'sha256WithRSAEncryption' or result.signature_algorithm == 'sha256WithECDSAEncryption'" + - "result.subject.commonName == 'www.example.com'" + - "result.subject.countryName == 'US'" + - "result.subject.localityName == 'Los Angeles'" # L + - "result.subject.organizationName == 'ACME Inc.'" + - "['organizationalUnitName', 'Pyrotechnics'] in result.subject_ordered" + - "['organizationalUnitName', 'Roadrunner pest control'] in result.subject_ordered" + - not result.expired + - result.version == 3 + - "'Digital Signature' in result.key_usage" + - "'IPSec User' in result.extended_key_usage" + - "'Biometric Info' in result.extended_key_usage" + +- name: (Selfsigned, {{select_crypto_backend}}) Create private key 3 + openssl_privatekey: + path: "{{ remote_tmp_dir }}/privatekey3.pem" + size: '{{ default_rsa_key_size_certifiates }}' + +- name: (Selfsigned, {{select_crypto_backend}}) Create CSR 3 + openssl_csr: + subject: + CN: www.example.com + privatekey_path: "{{ remote_tmp_dir }}/privatekey3.pem" + path: "{{ remote_tmp_dir }}/csr3.pem" + +- name: (Selfsigned, {{select_crypto_backend}}) Create certificate3 with notBefore and notAfter + x509_certificate: + provider: selfsigned + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + path: "{{ remote_tmp_dir }}/cert3.pem" + csr_path: "{{ remote_tmp_dir }}/csr3.pem" + privatekey_path: "{{ remote_tmp_dir }}/privatekey3.pem" + select_crypto_backend: '{{ select_crypto_backend }}' + +- name: (Selfsigned, {{select_crypto_backend}}) Generate privatekey + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + type: ECC + curve: "{{ (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') | ternary('secp521r1', 'secp256k1') }}" + # ^ cryptography on CentOS6 doesn't support secp256k1, so we use secp521r1 instead + +- name: (Selfsigned, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + subject: + commonName: www.example.com + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_ecc.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_certificate_ecc + +- name: (Selfsigned, {{select_crypto_backend}}) Generate CSR (privatekey passphrase) + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_pass.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + privatekey_passphrase: hunter2 + subject: + commonName: www.example.com + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (privatekey passphrase) + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_pass.pem' + csr_path: '{{ remote_tmp_dir }}/csr_pass.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + privatekey_passphrase: hunter2 + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_certificate_passphrase + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (failed passphrase 1) + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_pw1.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + privatekey_passphrase: hunter2 + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: passphrase_error_1 + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (failed passphrase 2) + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_pw2.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + privatekey_passphrase: wrong_password + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: passphrase_error_2 + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (failed passphrase 3) + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_pw3.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + ignore_errors: true + register: passphrase_error_3 + +- name: (Selfsigned, {{select_crypto_backend}}) Create broken certificate + copy: + dest: "{{ remote_tmp_dir }}/cert_broken.pem" + content: "broken" +- name: (Selfsigned, {{select_crypto_backend}}) Regenerate broken cert + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_broken.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + register: selfsigned_broken + +- name: (Selfsigned, {{select_crypto_backend}}) Backup test + x509_certificate: + path: '{{ remote_tmp_dir }}/selfsigned_cert_backup.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_backup_1 +- name: (Selfsigned, {{select_crypto_backend}}) Backup test (idempotent) + x509_certificate: + path: '{{ remote_tmp_dir }}/selfsigned_cert_backup.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_backup_2 +- name: (Selfsigned, {{select_crypto_backend}}) Backup test (change) + x509_certificate: + path: '{{ remote_tmp_dir }}/selfsigned_cert_backup.pem' + csr_path: '{{ remote_tmp_dir }}/csr.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_backup_3 +- name: (Selfsigned, {{select_crypto_backend}}) Backup test (remove) + x509_certificate: + path: '{{ remote_tmp_dir }}/selfsigned_cert_backup.pem' + state: absent + provider: selfsigned + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_backup_4 +- name: (Selfsigned, {{select_crypto_backend}}) Backup test (remove, idempotent) + x509_certificate: + path: '{{ remote_tmp_dir }}/selfsigned_cert_backup.pem' + state: absent + provider: selfsigned + backup: true + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_backup_5 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test + x509_certificate: + path: '{{ remote_tmp_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_subject_key_identifier_1 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (idempotency) + x509_certificate: + path: '{{ remote_tmp_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_subject_key_identifier_2 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (remove) + x509_certificate: + path: '{{ remote_tmp_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: never_create + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_subject_key_identifier_3 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (remove idempotency) + x509_certificate: + path: '{{ remote_tmp_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: never_create + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_subject_key_identifier_4 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (re-enable) + x509_certificate: + path: '{{ remote_tmp_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + register: selfsigned_subject_key_identifier_5 + +- name: (Selfsigned, {{select_crypto_backend}}) Ed25519 and Ed448 tests (for cryptography >= 2.6) + block: + - name: (Selfsigned, {{select_crypto_backend}}) Generate privatekeys + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_{{ item }}.pem' + type: '{{ item }}' + loop: + - Ed25519 + - Ed448 + register: selfsigned_certificate_ed25519_ed448_privatekey + ignore_errors: true + + - name: (Selfsigned, {{select_crypto_backend}}) Generate CSR etc. if private key generation succeeded + when: selfsigned_certificate_ed25519_ed448_privatekey is not failed + block: + + - name: (Selfsigned, {{select_crypto_backend}}) Generate CSR + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_{{ item }}.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + ignore_errors: true + + - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_{{ item }}.pem' + csr_path: '{{ remote_tmp_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_{{ item }}.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: selfsigned_certificate_ed25519_ed448 + ignore_errors: true + + - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate - idempotency + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_{{ item }}.pem' + csr_path: '{{ remote_tmp_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey_{{ item }}.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - Ed25519 + - Ed448 + register: selfsigned_certificate_ed25519_ed448_idempotence + ignore_errors: true + + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.6', '>=') + +- import_tasks: ../tests/validate_selfsigned.yml diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_ownca.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_ownca.yml new file mode 100644 index 000000000..b1569a94c --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_ownca.yml @@ -0,0 +1,191 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate (test - verify CA) + shell: '{{ openssl_binary }} verify -CAfile {{ remote_tmp_dir }}/ca_cert.pem {{ remote_tmp_dir }}/ownca_cert.pem | sed "s/.*: \(.*\)/\1/g"' + register: ownca_verify_ca + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate (test - ownca certificate modulus) + shell: '{{ openssl_binary }} x509 -noout -modulus -in {{ remote_tmp_dir }}/ownca_cert.pem' + register: ownca_cert_modulus + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate (test - ownca issuer value) + shell: '{{ openssl_binary }} x509 -noout -in {{ remote_tmp_dir}}/ownca_cert.pem -text | grep "Issuer" | sed "s/.*: \(.*\)/\1/g"' + register: ownca_cert_issuer + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate (test - ownca certficate version == default == 3) + shell: '{{ openssl_binary }} x509 -noout -in {{ remote_tmp_dir}}/ownca_cert.pem -text | grep "Version" | sed "s/.*: \(.*\) .*/\1/g"' + register: ownca_cert_version + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate (assert) + assert: + that: + - ownca_verify_ca.stdout == 'OK' + - ownca_cert_modulus.stdout == privatekey_modulus.stdout + - ownca_cert_version.stdout == '3' + # openssl 1.1.x adds a space between the output + - ownca_cert_issuer.stdout in ['CN=Example CA', 'CN = Example CA'] + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate idempotence + assert: + that: + - ownca_certificate.serial_number == ownca_certificate_idempotence.serial_number + - ownca_certificate.notBefore == ownca_certificate_idempotence.notBefore + - ownca_certificate.notAfter == ownca_certificate_idempotence.notAfter + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate regeneration + assert: + that: + - ownca_certificate_ca_subject_changed is changed + - ownca_certificate_ca_key_changed is changed + +- name: (OwnCA validation, {{select_crypto_backend}}) Read certificate + slurp: + src: '{{ remote_tmp_dir }}/ownca_cert.pem' + register: slurp + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca data return + assert: + that: + - ownca_certificate.certificate == (slurp.content | b64decode) + - ownca_certificate.certificate == ownca_certificate_idempotence.certificate + +- block: + - name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate v2 (test - ownca certificate version == 2) + shell: '{{ openssl_binary }} x509 -noout -in {{ remote_tmp_dir}}/ownca_cert_v2.pem -text | grep "Version" | sed "s/.*: \(.*\) .*/\1/g"' + register: ownca_cert_v2_version + + - name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate version 2 (assert) + assert: + that: + - ownca_cert_v2_version.stdout == '2' + when: "select_crypto_backend != 'cryptography'" + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate v2 (test - ownca certificate version == 2) + assert: + that: + - ownca_v2_certificate is failed + - "'The cryptography backend does not support v2 certificates' in ownca_v2_certificate.msg" + when: "select_crypto_backend == 'cryptography'" + + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate2 (test - ownca certificate modulus) + shell: '{{ openssl_binary }} x509 -noout -modulus -in {{ remote_tmp_dir }}/ownca_cert2.pem' + register: ownca_cert2_modulus + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate2 (assert) + assert: + that: + - ownca_cert2_modulus.stdout == privatekey2_modulus.stdout + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate owncal certificate3 (test - notBefore) + shell: '{{ openssl_binary }} x509 -noout -in {{ remote_tmp_dir }}/ownca_cert3.pem -text | grep "Not Before" | sed "s/.*: \(.*\) .*/\1/g"' + register: ownca_cert3_notBefore + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate3 (test - notAfter) + shell: '{{ openssl_binary }} x509 -noout -in {{ remote_tmp_dir }}/ownca_cert3.pem -text | grep "Not After" | sed "s/.*: \(.*\) .*/\1/g"' + register: ownca_cert3_notAfter + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate3 (assert - notBefore) + assert: + that: + - ownca_cert3_notBefore.stdout == 'Oct 23 13:37:42 2018' + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca certificate3 (assert - notAfter) + assert: + that: + - ownca_cert3_notAfter.stdout == 'Oct 23 13:37:42 2019' + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca ECC certificate (test - ownca certificate pubkey) + shell: '{{ openssl_binary }} x509 -noout -pubkey -in {{ remote_tmp_dir }}/ownca_cert_ecc.pem' + register: ownca_cert_ecc_pubkey + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca ECC certificate (test - ownca issuer value) + shell: '{{ openssl_binary }} x509 -noout -in {{ remote_tmp_dir}}/ownca_cert_ecc.pem -text | grep "Issuer" | sed "s/.*: \(.*\)/\1/g"' + register: ownca_cert_ecc_issuer + +- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca ECC certificate (assert) + assert: + that: + - ownca_cert_ecc_pubkey.stdout == privatekey_ecc_pubkey.stdout + # openssl 1.1.x adds a space between the output + - ownca_cert_ecc_issuer.stdout in ['CN=Example CA', 'CN = Example CA'] + +- name: (OwnCA validation, {{select_crypto_backend}}) + assert: + that: + - passphrase_error_1 is failed + - "'assphrase' in passphrase_error_1.msg or 'assword' in passphrase_error_1.msg" + - passphrase_error_2 is failed + - "'assphrase' in passphrase_error_2.msg or 'assword' in passphrase_error_2.msg or 'serializ' in passphrase_error_2.msg" + - passphrase_error_3 is failed + - "'assphrase' in passphrase_error_3.msg or 'assword' in passphrase_error_3.msg or 'serializ' in passphrase_error_3.msg" + +- name: (OwnCA validation, {{select_crypto_backend}})Verify that broken certificate will be regenerated + assert: + that: + - ownca_broken is changed + +- name: (OwnCA validation, {{select_crypto_backend}}) Check backup + assert: + that: + - ownca_backup_1 is changed + - ownca_backup_1.backup_file is undefined + - ownca_backup_2 is not changed + - ownca_backup_2.backup_file is undefined + - ownca_backup_3 is changed + - ownca_backup_3.backup_file is string + - ownca_backup_4 is changed + - ownca_backup_4.backup_file is string + - ownca_backup_5 is not changed + - ownca_backup_5.backup_file is undefined + +- name: (OwnCA validation, {{select_crypto_backend}}) Check create subject key identifier + assert: + that: + - ownca_subject_key_identifier_1 is changed + - ownca_subject_key_identifier_2 is not changed + - ownca_subject_key_identifier_3 is changed + - ownca_subject_key_identifier_4 is not changed + - ownca_subject_key_identifier_5 is changed + +- name: (OwnCA validation, {{select_crypto_backend}}) Check create authority key identifier + assert: + that: + - ownca_authority_key_identifier_1 is changed + - ownca_authority_key_identifier_2 is not changed + - ownca_authority_key_identifier_3 is changed + - ownca_authority_key_identifier_4 is not changed + - ownca_authority_key_identifier_5 is changed + +- name: (OwnCA validation, {{select_crypto_backend}}) Verify Ed25519 and Ed448 tests (for cryptography >= 2.6, < 2.8) + assert: + that: + - ownca_certificate_ed25519_ed448.results[0] is failed + - ownca_certificate_ed25519_ed448.results[1] is failed + - ownca_certificate_ed25519_ed448_idempotence.results[0] is failed + - ownca_certificate_ed25519_ed448_idempotence.results[1] is failed + - ownca_certificate_ed25519_ed448_2.results[0] is failed + - ownca_certificate_ed25519_ed448_2.results[1] is failed + - ownca_certificate_ed25519_ed448_2_idempotence.results[0] is failed + - ownca_certificate_ed25519_ed448_2_idempotence.results[1] is failed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.6', '>=') and cryptography_version.stdout is version('2.8', '<') and ownca_certificate_ed25519_ed448_privatekey is not failed + +- name: (OwnCA validation, {{select_crypto_backend}}) Verify Ed25519 and Ed448 tests (for cryptography >= 2.8) + assert: + that: + - ownca_certificate_ed25519_ed448 is succeeded + - ownca_certificate_ed25519_ed448.results[0] is changed + - ownca_certificate_ed25519_ed448.results[1] is changed + - ownca_certificate_ed25519_ed448_idempotence is succeeded + - ownca_certificate_ed25519_ed448_idempotence.results[0] is not changed + - ownca_certificate_ed25519_ed448_idempotence.results[1] is not changed + - ownca_certificate_ed25519_ed448_2 is succeeded + - ownca_certificate_ed25519_ed448_2.results[0] is changed + - ownca_certificate_ed25519_ed448_2.results[1] is changed + - ownca_certificate_ed25519_ed448_2_idempotence is succeeded + - ownca_certificate_ed25519_ed448_2_idempotence.results[0] is not changed + - ownca_certificate_ed25519_ed448_2_idempotence.results[1] is not changed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.8', '>=') and ownca_certificate_ed25519_ed448_privatekey is not failed diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml new file mode 100644 index 000000000..dfb1d8713 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml @@ -0,0 +1,211 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate (test - privatekey modulus) + shell: '{{ openssl_binary }} rsa -noout -modulus -in {{ remote_tmp_dir }}/privatekey.pem' + register: privatekey_modulus + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate behavior for no CSR + assert: + that: + - selfsigned_certificate_no_csr is changed + - selfsigned_certificate_no_csr_idempotence is not changed + - selfsigned_certificate_no_csr_idempotence_check is not changed + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate with no CSR (test - certificate modulus) + shell: '{{ openssl_binary }} x509 -noout -modulus -in {{ remote_tmp_dir }}/cert_no_csr.pem' + register: cert_modulus + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate with no CSR (test - certficate version == default == 3) + shell: '{{ openssl_binary }} x509 -noout -in {{ remote_tmp_dir}}/cert_no_csr.pem -text | grep "Version" | sed "s/.*: \(.*\) .*/\1/g"' + register: cert_version + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate with no CSR (assert) + assert: + that: + - cert_modulus.stdout == privatekey_modulus.stdout + - cert_version.stdout == '3' + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate with no CSR idempotence + assert: + that: + - selfsigned_certificate_no_csr.serial_number == selfsigned_certificate_no_csr_idempotence.serial_number + - selfsigned_certificate_no_csr.notBefore == selfsigned_certificate_no_csr_idempotence.notBefore + - selfsigned_certificate_no_csr.notAfter == selfsigned_certificate_no_csr_idempotence.notAfter + +- name: (Selfsigned validation, {{select_crypto_backend}}) Read certificate with no CSR + slurp: + src: '{{ remote_tmp_dir }}/cert_no_csr.pem' + register: slurp + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate data retrieval with no CSR + assert: + that: + - selfsigned_certificate_no_csr.certificate == (slurp.content | b64decode) + - selfsigned_certificate_no_csr.certificate == selfsigned_certificate_no_csr_idempotence.certificate + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate (test - certificate modulus) + shell: '{{ openssl_binary }} x509 -noout -modulus -in {{ remote_tmp_dir }}/cert.pem' + register: cert_modulus + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate (test - issuer value) + shell: '{{ openssl_binary }} x509 -noout -in {{ remote_tmp_dir}}/cert.pem -text | grep "Issuer" | sed "s/.*: \(.*\)/\1/g; s/ //g;"' + register: cert_issuer + + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate (test - certficate version == default == 3) + shell: '{{ openssl_binary }} x509 -noout -in {{ remote_tmp_dir}}/cert.pem -text | grep "Version" | sed "s/.*: \(.*\) .*/\1/g"' + register: cert_version + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate (assert) + assert: + that: + - cert_modulus.stdout == privatekey_modulus.stdout + - cert_version.stdout == '3' + - cert_issuer.stdout == 'CN=www.example.com' + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate idempotence + assert: + that: + - selfsigned_certificate.serial_number == selfsigned_certificate_idempotence.serial_number + - selfsigned_certificate.notBefore == selfsigned_certificate_idempotence.notBefore + - selfsigned_certificate.notAfter == selfsigned_certificate_idempotence.notAfter + +- name: (Selfsigned validation, {{select_crypto_backend}}) Read certificate + slurp: + src: '{{ remote_tmp_dir }}/cert.pem' + register: slurp + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate data retrieval + assert: + that: + - selfsigned_certificate.certificate == (slurp.content | b64decode) + - selfsigned_certificate.certificate == selfsigned_certificate_idempotence.certificate + +- name: Make sure that changes in CSR are detected even if private key is specified + assert: + that: + - selfsigned_certificate_csr_minimal_change is changed + +- block: + - name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate v2 (test - certificate version == 2) + shell: '{{ openssl_binary }} x509 -noout -in {{ remote_tmp_dir}}/cert_v2.pem -text | grep "Version" | sed "s/.*: \(.*\) .*/\1/g"' + register: cert_v2_version + + - name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate version 2 (assert) + assert: + that: + - cert_v2_version.stdout == '2' + when: select_crypto_backend != 'cryptography' + +- block: + - name: (Selfsigned validateion, {{ select_crypto_backend }} Validate certificate v2 is failed + assert: + that: + - selfsigned_v2_cert is failed + - "'The cryptography backend does not support v2 certificates' in selfsigned_v2_cert.msg" + when: select_crypto_backend == 'cryptography' + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate2 (test - privatekey modulus) + shell: '{{ openssl_binary }} rsa -noout -modulus -in {{ remote_tmp_dir }}/privatekey2.pem' + register: privatekey2_modulus + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate2 (test - certificate modulus) + shell: '{{ openssl_binary }} x509 -noout -modulus -in {{ remote_tmp_dir }}/cert2.pem' + register: cert2_modulus + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate2 (assert) + assert: + that: + - cert2_modulus.stdout == privatekey2_modulus.stdout + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate3 (test - notBefore) + shell: '{{ openssl_binary }} x509 -noout -in {{ remote_tmp_dir }}/cert3.pem -text | grep "Not Before" | sed "s/.*: \(.*\) .*/\1/g"' + register: cert3_notBefore + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate3 (test - notAfter) + shell: '{{ openssl_binary }} x509 -noout -in {{ remote_tmp_dir }}/cert3.pem -text | grep "Not After" | sed "s/.*: \(.*\) .*/\1/g"' + register: cert3_notAfter + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate3 (assert - notBefore) + assert: + that: + - cert3_notBefore.stdout == 'Oct 23 13:37:42 2018' + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate3 (assert - notAfter) + assert: + that: + - cert3_notAfter.stdout == 'Oct 23 13:37:42 2019' + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate ECC certificate (test - privatekey's pubkey) + shell: '{{ openssl_binary }} ec -pubout -in {{ remote_tmp_dir }}/privatekey_ecc.pem' + register: privatekey_ecc_pubkey + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate ECC certificate (test - certificate pubkey) + shell: '{{ openssl_binary }} x509 -noout -pubkey -in {{ remote_tmp_dir }}/cert_ecc.pem' + register: cert_ecc_pubkey + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate ECC certificate (assert) + assert: + that: + - cert_ecc_pubkey.stdout == privatekey_ecc_pubkey.stdout + +- name: (Selfsigned validation, {{select_crypto_backend}}) + assert: + that: + - passphrase_error_1 is failed + - "'assphrase' in passphrase_error_1.msg or 'assword' in passphrase_error_1.msg" + - passphrase_error_2 is failed + - "'assphrase' in passphrase_error_2.msg or 'assword' in passphrase_error_2.msg or 'serializ' in passphrase_error_2.msg" + - passphrase_error_3 is failed + - "'assphrase' in passphrase_error_3.msg or 'assword' in passphrase_error_3.msg or 'serializ' in passphrase_error_3.msg" + +- name: (Selfsigned validation, {{select_crypto_backend}}) Verify that broken certificate will be regenerated + assert: + that: + - selfsigned_broken is changed + +- name: (Selfsigned validation, {{select_crypto_backend}}) Check backup + assert: + that: + - selfsigned_backup_1 is changed + - selfsigned_backup_1.backup_file is undefined + - selfsigned_backup_2 is not changed + - selfsigned_backup_2.backup_file is undefined + - selfsigned_backup_3 is changed + - selfsigned_backup_3.backup_file is string + - selfsigned_backup_4 is changed + - selfsigned_backup_4.backup_file is string + - selfsigned_backup_5 is not changed + - selfsigned_backup_5.backup_file is undefined + +- name: (Selfsigned validation, {{select_crypto_backend}}) Check create subject key identifier + assert: + that: + - selfsigned_subject_key_identifier_1 is changed + - selfsigned_subject_key_identifier_2 is not changed + - selfsigned_subject_key_identifier_3 is changed + - selfsigned_subject_key_identifier_4 is not changed + - selfsigned_subject_key_identifier_5 is changed + +- name: (Selfsigned validation, {{select_crypto_backend}}) Verify Ed25519 and Ed448 tests (for cryptography >= 2.6, < 2.8) + assert: + that: + - selfsigned_certificate_ed25519_ed448.results[0] is failed + - selfsigned_certificate_ed25519_ed448.results[1] is failed + - selfsigned_certificate_ed25519_ed448_idempotence.results[0] is failed + - selfsigned_certificate_ed25519_ed448_idempotence.results[1] is failed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.6', '>=') and cryptography_version.stdout is version('2.8', '<') and selfsigned_certificate_ed25519_ed448_privatekey is not failed + +- name: (Selfsigned validation, {{select_crypto_backend}}) Verify Ed25519 and Ed448 tests (for cryptography >= 2.8) + assert: + that: + - selfsigned_certificate_ed25519_ed448 is succeeded + - selfsigned_certificate_ed25519_ed448.results[0] is changed + - selfsigned_certificate_ed25519_ed448.results[1] is changed + - selfsigned_certificate_ed25519_ed448_idempotence is succeeded + - selfsigned_certificate_ed25519_ed448_idempotence.results[0] is not changed + - selfsigned_certificate_ed25519_ed448_idempotence.results[1] is not changed + when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.8', '>=') and selfsigned_certificate_ed25519_ed448_privatekey is not failed diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/aliases b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/files/cert1.pem b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/files/cert1.pem new file mode 100644 index 000000000..834eedc44 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/files/cert1.pem @@ -0,0 +1,45 @@ +-----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----- diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/meta/main.yml new file mode 100644 index 000000000..7c2b42405 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir + - prepare_jinja2_compat diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/tasks/impl.yml new file mode 100644 index 000000000..37ad5ce1b --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/tasks/impl.yml @@ -0,0 +1,217 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- debug: + msg: "Executing tests with backend {{ select_crypto_backend }}" + +- name: ({{select_crypto_backend}}) Get certificate info + x509_certificate_info: + path: '{{ remote_tmp_dir }}/cert_1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: ({{select_crypto_backend}}) Get certificate info (IDNA encoding) + x509_certificate_info: + path: '{{ remote_tmp_dir }}/cert_1.pem' + name_encoding: idna + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_idna + +- name: ({{select_crypto_backend}}) Get certificate info (Unicode encoding) + x509_certificate_info: + path: '{{ remote_tmp_dir }}/cert_1.pem' + name_encoding: unicode + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_unicode + +- name: Check whether issuer and subject and extensions behave as expected + assert: + that: + - result.issuer.organizationalUnitName == 'ACME Department' + - "['organizationalUnitName', 'Crypto Department'] in result.issuer_ordered" + - "['organizationalUnitName', 'ACME Department'] in result.issuer_ordered" + - result.subject.organizationalUnitName == 'ACME Department' + - "['organizationalUnitName', 'Crypto Department'] in result.subject_ordered" + - "['organizationalUnitName', 'ACME Department'] in result.subject_ordered" + - result.public_key_type == 'RSA' + - result.public_key_data.size == (default_rsa_key_size_certifiates | int) + - "result.subject_alt_name == [ + 'DNS:www.ansible.com', + 'DNS:' ~ ('öç' if cryptography_version.stdout is version('2.1', '<') else 'xn--7ca3a') ~ '.com', + 'DNS:' ~ ('www.öç' if cryptography_version.stdout is version('2.1', '<') else 'xn--74h') ~ '.com', + 'IP:1.2.3.4', + 'IP:::1', + 'email:test@example.org', + 'URI:https://example.org/test/index.html' + ]" + - "result_idna.subject_alt_name == [ + 'DNS:www.ansible.com', + 'DNS:xn--7ca3a.com', + 'DNS:' ~ ('www.xn--7ca3a' if cryptography_version.stdout is version('2.1', '<') else 'xn--74h') ~ '.com', + 'IP:1.2.3.4', + 'IP:::1', + 'email:test@example.org', + 'URI:https://example.org/test/index.html' + ]" + - "result_unicode.subject_alt_name == [ + 'DNS:www.ansible.com', + 'DNS:öç.com', + 'DNS:' ~ ('www.öç' if cryptography_version.stdout is version('2.1', '<') else '☺') ~ '.com', + 'IP:1.2.3.4', + 'IP:::1', + 'email:test@example.org', + 'URI:https://example.org/test/index.html' + ]" + # TLS Feature + - result.extensions_by_oid['1.3.6.1.5.5.7.1.24'].critical == false + - result.extensions_by_oid['1.3.6.1.5.5.7.1.24'].value == 'MAMCAQU=' + # Key Usage + - result.extensions_by_oid['2.5.29.15'].critical == true + - result.extensions_by_oid['2.5.29.15'].value in ['AwMA/4A=', 'AwMH/4A='] + # Subject Alternative Names + - result.extensions_by_oid['2.5.29.17'].critical == false + - > + result.extensions_by_oid['2.5.29.17'].value == ( + 'MIGCgg93d3cuYW5zaWJsZS5jb22CDXhuLS03Y2EzYS5jb22CEXd3dy54bi0tN2NhM2EuY29thwQBAgMEhxAAAAAAAAAAAAAAAAAAAAABgRB0ZXN0QGV4YW1wbGUub3JnhiNodHRwczovL2V4YW1wbGUub3JnL3Rlc3QvaW5kZXguaHRtbA==' + if cryptography_version.stdout is version('2.1', '<') else + 'MHyCD3d3dy5hbnNpYmxlLmNvbYINeG4tLTdjYTNhLmNvbYILeG4tLTc0aC5jb22HBAECAwSHEAAAAAAAAAAAAAAAAAAAAAGBEHRlc3RAZXhhbXBsZS5vcmeGI2h0dHBzOi8vZXhhbXBsZS5vcmcvdGVzdC9pbmRleC5odG1s' + ) + # Basic Constraints + - result.extensions_by_oid['2.5.29.19'].critical == true + - result.extensions_by_oid['2.5.29.19'].value == 'MAYBAf8CARc=' + # Extended Key Usage + - result.extensions_by_oid['2.5.29.37'].critical == false + - result.extensions_by_oid['2.5.29.37'].value == 'MHQGCCsGAQUFBwMBBggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUFBwMJBgRVHSUABggrBgEFBQcBAwYIKwYBBQUHAwoGCCsGAQUFBwMHBggrBgEFBQcBAg==' + +- name: Check SubjectKeyIdentifier and AuthorityKeyIdentifier + assert: + that: + - result.subject_key_identifier == "00:11:22:33" + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + # Subject Key Identifier + - result.extensions_by_oid['2.5.29.14'].critical == false + # Authority Key Identifier + - result.extensions_by_oid['2.5.29.35'].critical == false + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: cryptography_version.stdout is version('1.3', '>=') + +- name: ({{select_crypto_backend}}) Read file + slurp: + src: '{{ remote_tmp_dir }}/cert_1.pem' + register: slurp + +- name: ({{select_crypto_backend}}) Get certificate info directly + x509_certificate_info: + content: '{{ slurp.content | b64decode }}' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result_direct + +- name: ({{select_crypto_backend}}) Compare output of direct and loaded info + assert: + that: + - result == result_direct + +- name: ({{select_crypto_backend}}) Get certificate info + x509_certificate_info: + path: '{{ remote_tmp_dir }}/cert_2.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + valid_at: + today: "+0d" + past: "20190101235901Z" + twentydays: "+20d" + register: result +- assert: + that: + - result.valid_at.today + - not result.valid_at.past + - not result.valid_at.twentydays + +- name: ({{select_crypto_backend}}) Get certificate info + x509_certificate_info: + path: '{{ remote_tmp_dir }}/cert_3.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check AuthorityKeyIdentifier + assert: + that: + - result.authority_key_identifier is none + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: cryptography_version.stdout is version('1.3', '>=') + +- name: ({{select_crypto_backend}}) Get certificate info + x509_certificate_info: + path: '{{ remote_tmp_dir }}/cert_4.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result + +- name: Check AuthorityKeyIdentifier + assert: + that: + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer is none + - result.authority_cert_serial_number is none + when: cryptography_version.stdout is version('1.3', '>=') + +- name: Copy packed cert 1 to remote + copy: + src: cert1.pem + dest: '{{ remote_tmp_dir }}/packed-cert-1.pem' + +- name: ({{select_crypto_backend}}) Get certificate info for packaged cert 1 + x509_certificate_info: + path: '{{ remote_tmp_dir }}/packed-cert-1.pem' + select_crypto_backend: '{{ select_crypto_backend }}' + register: result +- name: Check extensions + assert: + that: + - "'ocsp_uri' in result" + - "result.ocsp_uri == 'http://ocsp.int-x3.letsencrypt.org'" + - "'issuer_uri' in result" + - "result.issuer_uri == 'http://cert.int-x3.letsencrypt.org/'" + - result.extensions_by_oid | length == 9 + # Precert Signed Certificate Timestamps + - result.extensions_by_oid['1.3.6.1.4.1.11129.2.4.2'].critical == false + - result.extensions_by_oid['1.3.6.1.4.1.11129.2.4.2'].value == 'BIHyAPAAdgDBFkrgp3LS1DktyArBB3DU8MSb3pkaSEDB+gdRZPYzYAAAAWTdAoU6AAAEAwBHMEUCIG5WpfKF536KKa9fnVlYbwcfrKh09Hi2MSRwU2kad49UAiEA4RUKjJOgw11IHFNdit+sy1RcCU3QCSOEQYrJ1/oPltAAdgApPFGWVMg5ZbqqUPxYB9S3b79Yeily3KTDDPTlRUf0eAAAAWTdAoc+AAAEAwBHMEUCIQCJjo75K4rVDSiWQe3XFLY6MiG3zcHQrKb0YhM17r1UKAIgGa8qMoN03DLp+Rm9nRJ9XLbTJz1vbuu9PyXUY741P8E=' + # Authority Information Access + - result.extensions_by_oid['1.3.6.1.5.5.7.1.1'].critical == false + - result.extensions_by_oid['1.3.6.1.5.5.7.1.1'].value == 'MGEwLgYIKwYBBQUHMAGGImh0dHA6Ly9vY3NwLmludC14My5sZXRzZW5jcnlwdC5vcmcwLwYIKwYBBQUHMAKGI2h0dHA6Ly9jZXJ0LmludC14My5sZXRzZW5jcnlwdC5vcmcv' + # Subject Key Identifier + - result.extensions_by_oid['2.5.29.14'].critical == false + - result.extensions_by_oid['2.5.29.14'].value == 'BBRtcOI/yg62Ehbu5vQzxMUUdBOYMw==' + # Key Usage (The certificate has 'AwIFoA==', while de-serializing and re-serializing yields 'AwIAoA=='!) + - result.extensions_by_oid['2.5.29.15'].critical == true + - result.extensions_by_oid['2.5.29.15'].value in ['AwIFoA==', 'AwIAoA=='] + # Subject Alternative Names + - result.extensions_by_oid['2.5.29.17'].critical == false + - result.extensions_by_oid['2.5.29.17'].value == 'MIIB5IIbY2VydC5pbnQteDEubGV0c2VuY3J5cHQub3JnghtjZXJ0LmludC14Mi5sZXRzZW5jcnlwdC5vcmeCG2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9yZ4IbY2VydC5pbnQteDQubGV0c2VuY3J5cHQub3JnghxjZXJ0LnJvb3QteDEubGV0c2VuY3J5cHQub3Jngh9jZXJ0LnN0YWdpbmcteDEubGV0c2VuY3J5cHQub3Jngh9jZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JngiBjZXJ0LnN0Zy1yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ISY3AubGV0c2VuY3J5cHQub3JnghpjcC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZ4ITY3BzLmxldHNlbmNyeXB0Lm9yZ4IbY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3Jnghtjcmwucm9vdC14MS5sZXRzZW5jcnlwdC5vcmeCD2xldHNlbmNyeXB0Lm9yZ4IWb3JpZ2luLmxldHNlbmNyeXB0Lm9yZ4IXb3JpZ2luMi5sZXRzZW5jcnlwdC5vcmeCFnN0YXR1cy5sZXRzZW5jcnlwdC5vcmeCE3d3dy5sZXRzZW5jcnlwdC5vcmc=' + # Basic Constraints + - result.extensions_by_oid['2.5.29.19'].critical == true + - result.extensions_by_oid['2.5.29.19'].value == 'MAA=' + # Certificate Policies + - result.extensions_by_oid['2.5.29.32'].critical == false + - result.extensions_by_oid['2.5.29.32'].value == 'MIHzMAgGBmeBDAECATCB5gYLKwYBBAGC3xMBAQEwgdYwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIGrBggrBgEFBQcCAjCBngyBm1RoaXMgQ2VydGlmaWNhdGUgbWF5IG9ubHkgYmUgcmVsaWVkIHVwb24gYnkgUmVseWluZyBQYXJ0aWVzIGFuZCBvbmx5IGluIGFjY29yZGFuY2Ugd2l0aCB0aGUgQ2VydGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vbGV0c2VuY3J5cHQub3JnL3JlcG9zaXRvcnkv' + # Authority Key Identifier + - result.extensions_by_oid['2.5.29.35'].critical == false + - result.extensions_by_oid['2.5.29.35'].value == 'MBaAFKhKamMEfd265tE5t6ZFZe/zqOyh' + # Extended Key Usage + - result.extensions_by_oid['2.5.29.37'].critical == false + - result.extensions_by_oid['2.5.29.37'].value == 'MBQGCCsGAQUFBwMBBggrBgEFBQcDAg==' +- name: Check fingerprints + assert: + that: + - (result.fingerprints.sha256 == '57:7c:f1:f5:dd:cc:6e:e9:f3:17:28:73:17:e4:25:c7:69:74:3e:f7:9a:df:58:20:7a:5a:e4:aa:de:bf:24:5b' if result.fingerprints.sha256 is defined else true) + - (result.fingerprints.sha1 == 'b7:79:64:f4:2b:e0:ae:45:74:d4:f3:08:f6:53:cb:39:26:fa:52:6b' if result.fingerprints.sha1 is defined else true) diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/tasks/main.yml new file mode 100644 index 000000000..d9a322ac4 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_info/tasks/main.yml @@ -0,0 +1,153 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Make sure the Python idna library is installed + pip: + name: idna + state: present + +- name: Generate privatekey + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey.pem' + size: '{{ default_rsa_key_size_certifiates }}' + +- name: Generate privatekey with password + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + select_crypto_backend: cryptography + size: '{{ default_rsa_key_size_certifiates }}' + +- name: Generate CSR 1 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_1.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + subject: + commonName: www.example.com + C: de + L: Somewhere + ST: Zurich + streetAddress: Welcome Street + O: Ansible + organizationalUnitName: + - Crypto Department + - ACME Department + serialNumber: "1234" + SN: Last Name + GN: First Name + title: Chief + pseudonym: test + UID: asdf + emailAddress: test@example.com + postalAddress: 1234 Somewhere + postalCode: "1234" + useCommonNameForSAN: false + key_usage: + - digitalSignature + - keyAgreement + - Non Repudiation + - Key Encipherment + - dataEncipherment + - Certificate Sign + - cRLSign + - Encipher Only + - decipherOnly + key_usage_critical: true + extended_key_usage: + - serverAuth # the same as "TLS Web Server Authentication" + - TLS Web Server Authentication + - TLS Web Client Authentication + - Code Signing + - E-mail Protection + - timeStamping + - OCSPSigning + - Any Extended Key Usage + - qcStatements + - DVCS + - IPSec User + - biometricInfo + subject_alt_name: + - "DNS:www.ansible.com" + - "DNS:öç.com" + # cryptography < 2.1 cannot handle certain Unicode characters + - "DNS:{{ 'www.öç' if cryptography_version.stdout is version('2.1', '<') else '☺' }}.com" + - "IP:1.2.3.4" + - "IP:::1" + - "email:test@example.org" + - "URI:https://example.org/test/index.html" + basic_constraints: + - "CA:TRUE" + - "pathlen:23" + basic_constraints_critical: true + ocsp_must_staple: true + subject_key_identifier: '{{ "00:11:22:33" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + +- name: Generate CSR 2 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_2.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekeypw.pem' + privatekey_passphrase: hunter2 + useCommonNameForSAN: false + basic_constraints: + - "CA:TRUE" + +- name: Generate CSR 3 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_3.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + useCommonNameForSAN: false + subject_alt_name: + - "DNS:*.ansible.com" + - "DNS:*.example.org" + - "IP:DEAD:BEEF::1" + basic_constraints: + - "CA:FALSE" + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + +- name: Generate CSR 4 + openssl_csr: + path: '{{ remote_tmp_dir }}/csr_4.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + useCommonNameForSAN: false + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + +- name: Generate selfsigned certificates + x509_certificate: + path: '{{ remote_tmp_dir }}/cert_{{ item }}.pem' + csr_path: '{{ remote_tmp_dir }}/csr_{{ item }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_not_after: "+10d" + selfsigned_not_before: "-3d" + loop: + - 1 + - 2 + - 3 + - 4 + +- name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + when: cryptography_version.stdout is version('1.6', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_pipe/aliases b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_pipe/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_pipe/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_pipe/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_pipe/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_pipe/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_pipe/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_pipe/tasks/impl.yml new file mode 100644 index 000000000..1bec4d21f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_pipe/tasks/impl.yml @@ -0,0 +1,241 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "({{ select_crypto_backend }}) Generate privatekey" + openssl_privatekey: + path: '{{ remote_tmp_dir }}/{{ item }}.pem' + size: '{{ default_rsa_key_size_certifiates }}' + loop: + - privatekey + - privatekey2 + +- name: "({{ select_crypto_backend }}) Generate CSRs" + openssl_csr: + privatekey_path: '{{ remote_tmp_dir }}/{{ item.key }}.pem' + path: '{{ remote_tmp_dir }}/{{ item.name }}.csr' + subject: + commonName: '{{ item.cn }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - name: cert + key: privatekey + cn: www.ansible.com + - name: cert-2 + key: privatekey + cn: ansible.com + - name: cert-3 + key: privatekey2 + cn: example.com + - name: cert-4 + key: privatekey2 + cn: example.org + +## Self Signed + +- name: "({{ select_crypto_backend }}) Generate self-signed certificate (check mode)" + x509_certificate_pipe: + provider: selfsigned + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + csr_path: '{{ remote_tmp_dir }}/cert.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: generate_certificate_check + +- name: "({{ select_crypto_backend }}) Generate self-signed certificate" + x509_certificate_pipe: + provider: selfsigned + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + csr_path: '{{ remote_tmp_dir }}/cert.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_certificate + +- name: "({{ select_crypto_backend }}) Generate self-signed certificate (idempotent)" + x509_certificate_pipe: + provider: selfsigned + content: "{{ generate_certificate.certificate }}" + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + csr_path: '{{ remote_tmp_dir }}/cert.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_certificate_idempotent + +- name: "({{ select_crypto_backend }}) Generate self-signed certificate (idempotent, check mode)" + x509_certificate_pipe: + provider: selfsigned + content: "{{ generate_certificate.certificate }}" + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + csr_path: '{{ remote_tmp_dir }}/cert.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: generate_certificate_idempotent_check + +- name: "({{ select_crypto_backend }}) Generate self-signed certificate (changed)" + x509_certificate_pipe: + provider: selfsigned + content: "{{ generate_certificate.certificate }}" + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + csr_path: '{{ remote_tmp_dir }}/cert-2.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_certificate_changed + +- name: "({{ select_crypto_backend }}) Generate self-signed certificate (changed, check mode)" + x509_certificate_pipe: + provider: selfsigned + content: "{{ generate_certificate.certificate }}" + privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + csr_path: '{{ remote_tmp_dir }}/cert-2.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: generate_certificate_changed_check + +- name: "({{ select_crypto_backend }}) Validate certificate (test - privatekey modulus)" + shell: '{{ openssl_binary }} rsa -noout -modulus -in {{ remote_tmp_dir }}/privatekey.pem' + register: privatekey_modulus + +- name: "({{ select_crypto_backend }}) Validate certificate (test - Common Name)" + shell: "{{ openssl_binary }} x509 -noout -subject -in /dev/stdin -nameopt oneline,-space_eq" + args: + stdin: "{{ generate_certificate.certificate }}" + register: certificate_cn + +- name: "({{ select_crypto_backend }}) Validate certificate (test - certificate modulus)" + shell: '{{ openssl_binary }} x509 -noout -modulus -in /dev/stdin' + args: + stdin: "{{ generate_certificate.certificate }}" + register: certificate_modulus + +- name: "({{ select_crypto_backend }}) Validate certificate (assert)" + assert: + that: + - certificate_cn.stdout.split('=')[-1] == 'www.ansible.com' + - certificate_modulus.stdout == privatekey_modulus.stdout + +- name: "({{ select_crypto_backend }}) Validate certificate (check mode, idempotency)" + assert: + that: + - generate_certificate_check is changed + - generate_certificate is changed + - generate_certificate_idempotent is not changed + - generate_certificate_idempotent_check is not changed + - generate_certificate_changed is changed + - generate_certificate_changed_check is changed + +## Own CA + +- name: "({{ select_crypto_backend }}) Generate own CA certificate (check mode)" + x509_certificate_pipe: + provider: ownca + ownca_content: '{{ generate_certificate.certificate }}' + ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + csr_path: '{{ remote_tmp_dir }}/cert-3.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: ownca_generate_certificate_check + +- name: "({{ select_crypto_backend }}) Generate own CA certificate" + x509_certificate_pipe: + provider: ownca + ownca_content: '{{ generate_certificate.certificate }}' + ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + csr_path: '{{ remote_tmp_dir }}/cert-3.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_generate_certificate + +- name: "({{ select_crypto_backend }}) Generate own CA certificate (idempotent)" + x509_certificate_pipe: + provider: ownca + content: "{{ ownca_generate_certificate.certificate }}" + ownca_content: '{{ generate_certificate.certificate }}' + ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + csr_path: '{{ remote_tmp_dir }}/cert-3.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_generate_certificate_idempotent + +- name: "({{ select_crypto_backend }}) Generate own CA certificate (idempotent, check mode)" + x509_certificate_pipe: + provider: ownca + content: "{{ ownca_generate_certificate.certificate }}" + ownca_content: '{{ generate_certificate.certificate }}' + ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + csr_path: '{{ remote_tmp_dir }}/cert-3.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: ownca_generate_certificate_idempotent_check + +- name: "({{ select_crypto_backend }}) Generate own CA certificate (changed)" + x509_certificate_pipe: + provider: ownca + content: "{{ ownca_generate_certificate.certificate }}" + ownca_content: '{{ generate_certificate.certificate }}' + ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + csr_path: '{{ remote_tmp_dir }}/cert-4.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_generate_certificate_changed + +- name: "({{ select_crypto_backend }}) Generate own CA certificate (changed, check mode)" + x509_certificate_pipe: + provider: ownca + content: "{{ ownca_generate_certificate.certificate }}" + ownca_content: '{{ generate_certificate.certificate }}' + ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + csr_path: '{{ remote_tmp_dir }}/cert-4.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: true + register: ownca_generate_certificate_changed_check + +- name: "({{ select_crypto_backend }}) Validate certificate (test - privatekey modulus)" + shell: '{{ openssl_binary }} rsa -noout -modulus -in {{ remote_tmp_dir }}/privatekey2.pem' + register: privatekey_modulus + +- name: "({{ select_crypto_backend }}) Validate certificate (test - Common Name)" + shell: "{{ openssl_binary }} x509 -noout -subject -in /dev/stdin -nameopt oneline,-space_eq" + args: + stdin: "{{ ownca_generate_certificate.certificate }}" + register: certificate_cn + +- name: "({{ select_crypto_backend }}) Validate certificate (test - certificate modulus)" + shell: '{{ openssl_binary }} x509 -noout -modulus -in /dev/stdin' + args: + stdin: "{{ ownca_generate_certificate.certificate }}" + register: certificate_modulus + +- name: "({{ select_crypto_backend }}) Validate certificate (assert)" + assert: + that: + - certificate_cn.stdout.split('=')[-1] == 'example.com' + - certificate_modulus.stdout == privatekey_modulus.stdout + +- name: "({{ select_crypto_backend }}) Validate certificate (check mode, idempotency)" + assert: + that: + - ownca_generate_certificate_check is changed + - ownca_generate_certificate is changed + - ownca_generate_certificate_idempotent is not changed + - ownca_generate_certificate_idempotent_check is not changed + - ownca_generate_certificate_changed is changed + - ownca_generate_certificate_changed_check is changed diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_pipe/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_pipe/tasks/main.yml new file mode 100644 index 000000000..b8aeb8645 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate_pipe/tasks/main.yml @@ -0,0 +1,26 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Prepare private key for backend autodetection test + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_backend_selection.pem' + size: '{{ default_rsa_key_size_certifiates }}' +- name: Run module with backend autodetection + x509_certificate_pipe: + provider: selfsigned + privatekey_path: '{{ remote_tmp_dir }}/privatekey_backend_selection.pem' + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('1.6', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_crl/aliases b/ansible_collections/community/crypto/tests/integration/targets/x509_crl/aliases new file mode 100644 index 000000000..6f0b200f5 --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_crl/aliases @@ -0,0 +1,8 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +x509_crl_info +destructive diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_crl/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_crl/meta/main.yml new file mode 100644 index 000000000..54bf29e9f --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_crl/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_crl/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_crl/tasks/impl.yml new file mode 100644 index 000000000..11fa7dcca --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_crl/tasks/impl.yml @@ -0,0 +1,695 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create CRL 1 (check mode) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ remote_tmp_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: true + register: crl_1_check + +- name: Create CRL 1 + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ remote_tmp_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: crl_1 + +- assert: + that: + - crl_1_check is changed + - crl_1 is changed + +- name: Retrieve CRL 1 infos + x509_crl_info: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + register: crl_1_info_1 + +- name: Read ca-crl1.crl + slurp: + src: '{{ remote_tmp_dir }}/ca-crl1.crl' + register: slurp + +- name: Retrieve CRL 1 infos via file content + x509_crl_info: + content: '{{ slurp.content | b64decode }}' + register: crl_1_info_2 + +- name: Retrieve CRL 1 infos via file content (Base64) + x509_crl_info: + content: '{{ slurp.content }}' + register: crl_1_info_3 + +- name: Create CRL 1 (idempotent, check mode) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ remote_tmp_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: true + register: crl_1_idem_check + +- name: Create CRL 1 (idempotent) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ remote_tmp_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: crl_1_idem + +- name: Read file + slurp: + src: '{{ remote_tmp_dir }}/{{ item }}' + loop: + - ca.key + - cert-1.pem + - cert-2.pem + register: slurp + +- name: Create CRL 1 (idempotent with content, check mode) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + privatekey_content: "{{ slurp.results[0].content | b64decode }}" + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - content: "{{ slurp.results[1].content | b64decode }}" + revocation_date: 20191013000000Z + - content: "{{ slurp.results[2].content | b64decode }}" + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: true + register: crl_1_idem_content_check + +- name: Create CRL 1 (idempotent with content) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + privatekey_content: "{{ slurp.results[0].content | b64decode }}" + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - content: "{{ slurp.results[1].content | b64decode }}" + revocation_date: 20191013000000Z + - content: "{{ slurp.results[2].content | b64decode }}" + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: crl_1_idem_content + +- name: Create CRL 1 (format, check mode) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + format: der + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ remote_tmp_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: true + register: crl_1_format_check + +- name: Create CRL 1 (format) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + format: der + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ remote_tmp_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: crl_1_format + +- name: Create CRL 1 (format, idempotent, check mode) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + format: der + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ remote_tmp_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: true + register: crl_1_format_idem_check + +- name: Create CRL 1 (format, idempotent) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + format: der + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ remote_tmp_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + return_content: true + register: crl_1_format_idem + +- name: Retrieve CRL 1 infos via file + x509_crl_info: + path: '{{ remote_tmp_dir }}/ca-crl1.crl' + register: crl_1_info_4 + +- name: Read ca-crl1.crl + slurp: + src: "{{ remote_tmp_dir }}/ca-crl1.crl" + register: content + +- name: Retrieve CRL 1 infos via file content (Base64) + x509_crl_info: + content: '{{ content.content }}' + register: crl_1_info_5 + +- name: Create CRL 2 (check mode) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - CN: CRL + - countryName: US + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + - path: '{{ remote_tmp_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + check_mode: true + register: crl_2_check + +- name: Create CRL 2 + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - CN: CRL + - countryName: US + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + - path: '{{ remote_tmp_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + register: crl_2 + +- name: Create CRL 2 (idempotent, check mode) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - CN: CRL + - C: US + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + - path: '{{ remote_tmp_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + ignore_timestamps: true + check_mode: true + register: crl_2_idem_check + +- name: Create CRL 2 (idempotent) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - CN: CRL + - countryName: US + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + - path: '{{ remote_tmp_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + ignore_timestamps: true + register: crl_2_idem + +- name: Create CRL 2 (idempotent update, check mode) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - CN: CRL + - countryName: US + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - serial_number: 1235 + ignore_timestamps: true + crl_mode: update + check_mode: true + register: crl_2_idem_update_change_check + +- name: Create CRL 2 (idempotent update) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - CN: CRL + - countryName: US + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - serial_number: 1235 + ignore_timestamps: true + crl_mode: update + register: crl_2_idem_update_change + +- name: Create CRL 2 (idempotent update, check mode) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - CN: CRL + - countryName: US + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + ignore_timestamps: true + crl_mode: update + check_mode: true + register: crl_2_idem_update_check + +- name: Create CRL 2 (idempotent update) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - CN: CRL + - countryName: US + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + ignore_timestamps: true + crl_mode: update + register: crl_2_idem_update + +- name: Create CRL 2 (changed timestamps, check mode) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - CN: CRL + - countryName: US + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + ignore_timestamps: false + crl_mode: update + check_mode: true + register: crl_2_change_check + +- name: Create CRL 2 (changed timestamps) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - CN: CRL + - countryName: US + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + ignore_timestamps: false + crl_mode: update + return_content: true + register: crl_2_change + +- name: Read ca-crl2.crl + slurp: + src: '{{ remote_tmp_dir }}/ca-crl2.crl' + register: slurp_crl2_1 + +- name: Retrieve CRL 2 infos + x509_crl_info: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + list_revoked_certificates: false + register: crl_2_info_1 + +- name: Create CRL 2 (changed order, should be ignored) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer: + countryName: US + CN: + - Ansible + - CRL + - Test + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + ignore_timestamps: true + crl_mode: update + return_content: true + register: crl_2_change_order_ignore + +- name: Create CRL 2 (changed order) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer_ordered: + - CN: Ansible + - countryName: US + - CN: CRL + - CN: Test + last_update: +0d + next_update: +0d + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-2.pem' + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + ignore_timestamps: true + crl_mode: update + return_content: true + register: crl_2_change_order + +- name: Read ca-crl2.crl + slurp: + src: '{{ remote_tmp_dir }}/ca-crl2.crl' + register: slurp_crl2_2 + +- name: Retrieve CRL 2 infos again + x509_crl_info: + path: '{{ remote_tmp_dir }}/ca-crl2.crl' + list_revoked_certificates: false + register: crl_2_info_2 + +- name: Create CRL 3 + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl3.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - serial_number: 1234 + revocation_date: 20191001000000Z + # * cryptography < 2.1 strips username and password from URIs. To avoid problems, we do + # not pass usernames and passwords for URIs when the cryptography version is < 2.1. + # * Python 3.5 before 3.5.8 rc 1 has a bug in urllib.parse.urlparse() that results in an + # error if a Unicode netloc has a username or password included. + # (https://github.com/ansible-collections/community.crypto/pull/436#issuecomment-1101737134) + # This affects the Python 3.5 included in Ansible 2.9's default test container; to avoid + # this, we also do not pass usernames and passwords for Python 3.5. + issuer: + - "DNS:ca.example.org" + - "DNS:ffóò.ḃâŗ.çøṁ" + - "email:foo@ḃâŗ.çøṁ" + - "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'admin:hunter2@' }}ffóò.ḃâŗ.çøṁ/baz?foo=bar" + - "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'goo@' }}www.straße.de" + - "URI:https://straße.de:8080" + - "URI:http://gefäß.org" + - "URI:http://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'a:b@' }}ä:1" + issuer_critical: true + register: crl_3 + +- name: Create CRL 3 (IDNA encoding) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl3.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - serial_number: 1234 + revocation_date: 20191001000000Z + issuer: + - "DNS:ca.example.org" + - "DNS:xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n" + - "email:foo@xn--2ca8uh37e.xn--7ca8a981n" + - "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'admin:hunter2@' }}xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n/baz?foo=bar" + - "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'goo@' }}www.xn--strae-oqa.de" + - "URI:https://xn--strae-oqa.de:8080" + - "URI:http://xn--gef-7kay.org" + - "URI:http://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'a:b@' }}xn--4ca:1" + issuer_critical: true + ignore_timestamps: true + name_encoding: idna + register: crl_3_idna + +- name: Create CRL 3 (Unicode encoding) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl3.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + issuer: + CN: Ansible + last_update: +0d + next_update: +0d + revoked_certificates: + - serial_number: 1234 + revocation_date: 20191001000000Z + issuer: + - "DNS:ca.example.org" + - "DNS:ffóò.ḃâŗ.çøṁ" + - "email:foo@ḃâŗ.çøṁ" + - "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'admin:hunter2@' }}ffóò.ḃâŗ.çøṁ/baz?foo=bar" + - "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'goo@' }}www.straße.de" + - "URI:https://straße.de:8080" + - "URI:http://gefäß.org" + - "URI:http://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'a:b@' }}ä:1" + issuer_critical: true + ignore_timestamps: true + name_encoding: unicode + register: crl_3_unicode + +- name: Retrieve CRL 3 infos + x509_crl_info: + path: '{{ remote_tmp_dir }}/ca-crl3.crl' + list_revoked_certificates: true + register: crl_3_info + +- name: Retrieve CRL 3 infos (IDNA encoding) + x509_crl_info: + path: '{{ remote_tmp_dir }}/ca-crl3.crl' + name_encoding: idna + list_revoked_certificates: true + register: crl_3_info_idna + +- name: Retrieve CRL 3 infos (Unicode encoding) + x509_crl_info: + path: '{{ remote_tmp_dir }}/ca-crl3.crl' + name_encoding: unicode + list_revoked_certificates: true + register: crl_3_info_unicode + +- name: Ed25519 and Ed448 tests (for cryptography >= 2.6) + block: + - name: Generate private keys + openssl_privatekey: + path: '{{ remote_tmp_dir }}/ca-{{ item }}.key' + type: '{{ item }}' + loop: + - Ed25519 + - Ed448 + register: ed25519_ed448_privatekey + ignore_errors: true + + - when: ed25519_ed448_privatekey is not failed + block: + + - name: Create CRL + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl-{{ item }}.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca-{{ item }}.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ remote_tmp_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: ed25519_ed448_crl + loop: + - Ed25519 + - Ed448 + ignore_errors: true + + - name: Create CRL (idempotence) + x509_crl: + path: '{{ remote_tmp_dir }}/ca-crl-{{ item }}.crl' + privatekey_path: '{{ remote_tmp_dir }}/ca-{{ item }}.key' + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ remote_tmp_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ remote_tmp_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: true + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: ed25519_ed448_crl_idempotence + loop: + - Ed25519 + - Ed448 + ignore_errors: true + + when: cryptography_version.stdout is version('2.6', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_crl/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_crl/tasks/main.yml new file mode 100644 index 000000000..6014722fa --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_crl/tasks/main.yml @@ -0,0 +1,93 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Make sure the Python idna library is installed + pip: + name: idna + state: present + +- set_fact: + certificates: + - name: ca + subject: + commonName: Ansible + is_ca: true + - name: ca-2 + subject: + commonName: Ansible Other CA + is_ca: true + - 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: '{{ remote_tmp_dir }}/{{ item.name }}.key' + type: ECC + curve: secp256r1 + loop: "{{ certificates }}" + +- name: Generate CSRs + openssl_csr: + path: '{{ remote_tmp_dir }}/{{ item.name }}.csr' + privatekey_path: '{{ remote_tmp_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: false + loop: "{{ certificates }}" + +- name: Generate CA certificates + x509_certificate: + path: '{{ remote_tmp_dir }}/{{ item.name }}.pem' + csr_path: '{{ remote_tmp_dir }}/{{ item.name }}.csr' + privatekey_path: '{{ remote_tmp_dir }}/{{ item.name }}.key' + provider: selfsigned + loop: "{{ certificates }}" + when: item.is_ca | default(false) + +- name: Generate other certificates + x509_certificate: + path: '{{ remote_tmp_dir }}/{{ item.name }}.pem' + csr_path: '{{ remote_tmp_dir }}/{{ item.name }}.csr' + provider: ownca + ownca_path: '{{ remote_tmp_dir }}/ca.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca.key' + loop: "{{ certificates }}" + when: not (item.is_ca | default(false)) + +- name: Get certificate infos + x509_certificate_info: + path: '{{ remote_tmp_dir }}/{{ item }}.pem' + loop: + - cert-1 + - cert-2 + - cert-3 + - cert-4 + register: certificate_infos + +- block: + - name: Running tests + include_tasks: impl.yml + + - import_tasks: ../tests/validate.yml + + when: cryptography_version.stdout is version('1.2', '>=') diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_crl/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_crl/tests/validate.yml new file mode 100644 index 000000000..77e4aefae --- /dev/null +++ b/ansible_collections/community/crypto/tests/integration/targets/x509_crl/tests/validate.yml @@ -0,0 +1,203 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Validate CRL 1 + assert: + that: + - crl_1_check is changed + - crl_1 is changed + - crl_1_idem_check is not changed + - crl_1_idem is not changed + - crl_1_idem_content_check is not changed + - crl_1_idem_content is not changed + +- name: Validate CRL 1 info + assert: + that: + - crl_1_info_1.format == 'pem' + - crl_1_info_1.digest == 'ecdsa-with-SHA256' + - crl_1_info_1.issuer | length == 1 + - crl_1_info_1.issuer.commonName == 'Ansible' + - crl_1_info_1.issuer_ordered | length == 1 + - crl_1_info_1.last_update == '20191013000000Z' + - crl_1_info_1.next_update == '20191113000000Z' + - crl_1_info_1.revoked_certificates | length == 3 + - crl_1_info_1.revoked_certificates[0].invalidity_date is none + - crl_1_info_1.revoked_certificates[0].invalidity_date_critical == false + - crl_1_info_1.revoked_certificates[0].issuer is none + - crl_1_info_1.revoked_certificates[0].issuer_critical == false + - crl_1_info_1.revoked_certificates[0].reason is none + - crl_1_info_1.revoked_certificates[0].reason_critical == false + - crl_1_info_1.revoked_certificates[0].revocation_date == '20191013000000Z' + - crl_1_info_1.revoked_certificates[0].serial_number == certificate_infos.results[0].serial_number + - crl_1_info_1.revoked_certificates[1].invalidity_date == '20191012000000Z' + - crl_1_info_1.revoked_certificates[1].invalidity_date_critical == false + - crl_1_info_1.revoked_certificates[1].issuer is none + - crl_1_info_1.revoked_certificates[1].issuer_critical == false + - crl_1_info_1.revoked_certificates[1].reason == 'key_compromise' + - crl_1_info_1.revoked_certificates[1].reason_critical == true + - crl_1_info_1.revoked_certificates[1].revocation_date == '20191013000000Z' + - crl_1_info_1.revoked_certificates[1].serial_number == certificate_infos.results[1].serial_number + - crl_1_info_1.revoked_certificates[2].invalidity_date is none + - crl_1_info_1.revoked_certificates[2].invalidity_date_critical == false + - crl_1_info_1.revoked_certificates[2].issuer is none + - crl_1_info_1.revoked_certificates[2].issuer_critical == false + - crl_1_info_1.revoked_certificates[2].reason is none + - crl_1_info_1.revoked_certificates[2].reason_critical == false + - crl_1_info_1.revoked_certificates[2].revocation_date == '20191001000000Z' + - crl_1_info_1.revoked_certificates[2].serial_number == 1234 + - crl_1_info_1 == crl_1_info_2 + - crl_1_info_1 == crl_1_info_3 + +- name: Validate CRL 1 + assert: + that: + - crl_1_format_check is changed + - crl_1_format is changed + - crl_1_format_idem_check is not changed + - crl_1_format_idem is not changed + - crl_1_info_4.format == 'der' + - crl_1_info_5.format == 'der' + +- name: Read ca-crl1.crl + slurp: + src: "{{ remote_tmp_dir }}/ca-crl1.crl" + register: content +- name: Validate CRL 1 Base64 content + assert: + that: + - crl_1_format_idem.crl | b64decode == content.content | b64decode + +- name: Validate CRL 2 + assert: + that: + - crl_2_check is changed + - crl_2 is changed + - crl_2_idem_check is not changed + - crl_2_idem is not changed + - crl_2_idem_update_change_check is changed + - crl_2_idem_update_change is changed + - crl_2_idem_update_check is not changed + - crl_2_idem_update is not changed + - crl_2_change_check is changed + - crl_2_change is changed + - crl_2_change.crl == (slurp_crl2_1.content | b64decode) + - crl_2_change_order_ignore is not changed + - crl_2_change_order is changed + - crl_2_change_order.crl == (slurp_crl2_2.content | b64decode) + +- name: Validate CRL 2 info + assert: + that: + - "'revoked_certificates' not in crl_2_info_1" + - > + crl_2_info_1.issuer_ordered == [ + ['commonName', 'Ansible'], + ['commonName', 'CRL'], + ['countryName', 'US'], + ['commonName', 'Test'], + ] + - > + crl_2_info_2.issuer_ordered == [ + ['commonName', 'Ansible'], + ['countryName', 'US'], + ['commonName', 'CRL'], + ['commonName', 'Test'], + ] + +- name: Validate CRL 3 info + assert: + that: + - crl_3.revoked_certificates == crl_3_info.revoked_certificates + - crl_3.revoked_certificates[0].issuer == ([ + "DNS:ca.example.org", + "DNS:ffóò.ḃâŗ.çøṁ", + "email:foo@ḃâŗ.çøṁ", + "URI:https://ffóò.ḃâŗ.çøṁ/baz?foo=bar", + "URI:https://www.straße.de", + "URI:https://straße.de:8080", + "URI:http://gefäß.org", + "URI:http://ä:1", + ] if cryptography_version.stdout is version('2.1', '<') else [ + "DNS:ca.example.org", + "DNS:xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n", + "email:foo@xn--2ca8uh37e.xn--7ca8a981n", + "URI:https://xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n/baz?foo=bar", + "URI:https://www.xn--strae-oqa.de", + "URI:https://xn--strae-oqa.de:8080", + "URI:http://xn--gef-7kay.org", + "URI:http://xn--4ca:1", + ] if ansible_facts.python.version.minor == 5 else [ + "DNS:ca.example.org", + "DNS:xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n", + "email:foo@xn--2ca8uh37e.xn--7ca8a981n", + "URI:https://admin:hunter2@xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n/baz?foo=bar", + "URI:https://goo@www.xn--strae-oqa.de", + "URI:https://xn--strae-oqa.de:8080", + "URI:http://xn--gef-7kay.org", + "URI:http://a:b@xn--4ca:1", + ]) + - crl_3_idna is not changed + - crl_3_idna.revoked_certificates == crl_3_info_idna.revoked_certificates + - crl_3_idna.revoked_certificates[0].issuer == ([ + "DNS:ca.example.org", + "DNS:xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n", + "email:foo@xn--2ca8uh37e.xn--7ca8a981n", + "URI:https://xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n/baz?foo=bar", + "URI:https://www.xn--strae-oqa.de", + "URI:https://xn--strae-oqa.de:8080", + "URI:http://xn--gef-7kay.org", + "URI:http://xn--4ca:1", + ] if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else [ + "DNS:ca.example.org", + "DNS:xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n", + "email:foo@xn--2ca8uh37e.xn--7ca8a981n", + "URI:https://admin:hunter2@xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n/baz?foo=bar", + "URI:https://goo@www.xn--strae-oqa.de", + "URI:https://xn--strae-oqa.de:8080", + "URI:http://xn--gef-7kay.org", + "URI:http://a:b@xn--4ca:1", + ]) + - crl_3_unicode is not changed + - crl_3_unicode.revoked_certificates == crl_3_info_unicode.revoked_certificates + - crl_3_unicode.revoked_certificates[0].issuer == ([ + "DNS:ca.example.org", + "DNS:ffóò.ḃâŗ.çøṁ", + "email:foo@ḃâŗ.çøṁ", + "URI:https://ffóò.ḃâŗ.çøṁ/baz?foo=bar", + "URI:https://www.straße.de", + "URI:https://straße.de:8080", + "URI:http://gefäß.org", + "URI:http://ä:1", + ] if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else [ + "DNS:ca.example.org", + "DNS:ffóò.ḃâŗ.çøṁ", + "email:foo@ḃâŗ.çøṁ", + "URI:https://admin:hunter2@ffóò.ḃâŗ.çøṁ/baz?foo=bar", + "URI:https://goo@www.straße.de", + "URI:https://straße.de:8080", + "URI:http://gefäß.org", + "URI:http://a:b@ä:1", + ]) + +- name: Verify Ed25519 and Ed448 tests (for cryptography >= 2.6, < 2.8) + assert: + that: + - ed25519_ed448_crl.results[0] is failed + - ed25519_ed448_crl.results[1] is failed + - ed25519_ed448_crl_idempotence.results[0] is failed + - ed25519_ed448_crl_idempotence.results[1] is failed + when: cryptography_version.stdout is version('2.6', '>=') and cryptography_version.stdout is version('2.8', '<') and ed25519_ed448_privatekey is not failed + +- name: Verify Ed25519 and Ed448 tests (for cryptography >= 2.8) + assert: + that: + - ed25519_ed448_crl is succeeded + - ed25519_ed448_crl.results[0] is changed + - ed25519_ed448_crl.results[1] is changed + - ed25519_ed448_crl_idempotence is succeeded + - ed25519_ed448_crl_idempotence.results[0] is not changed + - ed25519_ed448_crl_idempotence.results[1] is not changed + when: cryptography_version.stdout is version('2.8', '>=') and ed25519_ed448_privatekey is not failed diff --git a/ansible_collections/community/crypto/tests/sanity/extra/extra-docs.json b/ansible_collections/community/crypto/tests/sanity/extra/extra-docs.json new file mode 100644 index 000000000..9a28d174f --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/extra/extra-docs.json @@ -0,0 +1,13 @@ +{ + "include_symlinks": false, + "prefixes": [ + "docs/docsite/", + "plugins/", + "roles/" + ], + "output": "path-line-column-message", + "requirements": [ + "ansible-core", + "antsibull-docs" + ] +} diff --git a/ansible_collections/community/crypto/tests/sanity/extra/extra-docs.json.license b/ansible_collections/community/crypto/tests/sanity/extra/extra-docs.json.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/extra/extra-docs.json.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/sanity/extra/extra-docs.py b/ansible_collections/community/crypto/tests/sanity/extra/extra-docs.py new file mode 100755 index 000000000..c636beb08 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/extra/extra-docs.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +"""Check extra collection docs with antsibull-docs.""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import sys +import subprocess + + +def main(): + """Main entry point.""" + env = os.environ.copy() + suffix = ':{env}'.format(env=env["ANSIBLE_COLLECTIONS_PATH"]) if 'ANSIBLE_COLLECTIONS_PATH' in env else '' + env['ANSIBLE_COLLECTIONS_PATH'] = '{root}{suffix}'.format(root=os.path.dirname(os.path.dirname(os.path.dirname(os.getcwd()))), suffix=suffix) + p = subprocess.run( + ['antsibull-docs', 'lint-collection-docs', '--plugin-docs', '--disallow-semantic-markup', '--skip-rstcheck', '.'], + env=env, + check=False, + ) + if p.returncode not in (0, 3): + print('{0}:0:0: unexpected return code {1}'.format(sys.argv[0], p.returncode)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/tests/sanity/extra/licenses.json b/ansible_collections/community/crypto/tests/sanity/extra/licenses.json new file mode 100644 index 000000000..50e47ca88 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/extra/licenses.json @@ -0,0 +1,4 @@ +{ + "include_symlinks": false, + "output": "path-message" +} diff --git a/ansible_collections/community/crypto/tests/sanity/extra/licenses.json.license b/ansible_collections/community/crypto/tests/sanity/extra/licenses.json.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/extra/licenses.json.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/sanity/extra/licenses.py b/ansible_collections/community/crypto/tests/sanity/extra/licenses.py new file mode 100755 index 000000000..9eb3cc9ec --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/extra/licenses.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +"""Prevent files without a correct license identifier from being added to the source tree.""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import glob +import sys + + +def format_license_list(licenses): + if not licenses: + return '(empty)' + return ', '.join(['"%s"' % license for license in licenses]) + + +def find_licenses(filename, relax=False): + spdx_license_identifiers = [] + other_license_identifiers = [] + has_copyright = False + try: + with open(filename, 'r', encoding='utf-8') as f: + for line in f: + line = line.rstrip() + if 'Copyright ' in line: + has_copyright = True + if 'Copyright: ' in line: + print('%s: found copyright line with "Copyright:". Please remove the colon.' % (filename, )) + if 'SPDX-FileCopyrightText: ' in line: + has_copyright = True + idx = line.find('SPDX-License-Identifier: ') + if idx >= 0: + lic_id = line[idx + len('SPDX-License-Identifier: '):] + spdx_license_identifiers.extend(lic_id.split(' OR ')) + if 'GNU General Public License' in line: + if 'v3.0+' in line: + other_license_identifiers.append('GPL-3.0-or-later') + if 'version 3 or later' in line: + other_license_identifiers.append('GPL-3.0-or-later') + if 'Simplified BSD License' in line: + other_license_identifiers.append('BSD-2-Clause') + if 'Apache License 2.0' in line: + other_license_identifiers.append('Apache-2.0') + if 'PSF License' in line or 'Python-2.0' in line: + other_license_identifiers.append('PSF-2.0') + if 'MIT License' in line: + other_license_identifiers.append('MIT') + except Exception as exc: + print('%s: error while processing file: %s' % (filename, exc)) + if len(set(spdx_license_identifiers)) < len(spdx_license_identifiers): + print('%s: found identical SPDX-License-Identifier values' % (filename, )) + if other_license_identifiers and set(other_license_identifiers) != set(spdx_license_identifiers): + print('%s: SPDX-License-Identifier yielded the license list %s, while manual guessing yielded the license list %s' % ( + filename, format_license_list(spdx_license_identifiers), format_license_list(other_license_identifiers))) + if not has_copyright and not relax: + print('%s: found no copyright notice' % (filename, )) + return sorted(spdx_license_identifiers) + + +def main(): + """Main entry point.""" + paths = sys.argv[1:] or sys.stdin.read().splitlines() + + # The following paths are allowed to have no license identifier + no_comments_allowed = [ + 'changelogs/fragments/*.yml', + 'changelogs/fragments/*.yaml', + ] + + # These files are completely ignored + ignore_paths = [ + '.ansible-test-timeout.json', + '.reuse/dep5', + 'LICENSES/*.txt', + 'COPYING', + 'tests/integration/targets/*/files/*.pem', + 'tests/integration/targets/*/files/roots/*.pem', + ] + + no_comments_allowed = [fn for pattern in no_comments_allowed for fn in glob.glob(pattern)] + ignore_paths = [fn for pattern in ignore_paths for fn in glob.glob(pattern)] + + valid_licenses = [license_file[len('LICENSES/'):-len('.txt')] for license_file in glob.glob('LICENSES/*.txt')] + + for path in paths: + if path.startswith('./'): + path = path[2:] + if path in ignore_paths or path.startswith('tests/output/'): + continue + if os.stat(path).st_size == 0: + continue + if not path.endswith('.license') and os.path.exists(path + '.license'): + path = path + '.license' + valid_licenses_for_path = valid_licenses + if path.startswith('plugins/') and not path.startswith(('plugins/modules/', 'plugins/module_utils/')): + valid_licenses_for_path = [license for license in valid_licenses if license == 'GPL-3.0-or-later'] + licenses = find_licenses(path, relax=path in no_comments_allowed) + if not licenses: + if path not in no_comments_allowed: + print('%s: must have at least one license' % (path, )) + else: + for license in licenses: + if license not in valid_licenses_for_path: + print('%s: found not allowed license "%s", must be one of %s' % ( + path, license, format_license_list(valid_licenses_for_path))) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/tests/sanity/extra/licenses.py.license b/ansible_collections/community/crypto/tests/sanity/extra/licenses.py.license new file mode 100644 index 000000000..6c4958feb --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/extra/licenses.py.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: 2022, Felix Fontein <felix@fontein.de> diff --git a/ansible_collections/community/crypto/tests/sanity/extra/no-unwanted-files.json b/ansible_collections/community/crypto/tests/sanity/extra/no-unwanted-files.json new file mode 100644 index 000000000..c789a7fd3 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/extra/no-unwanted-files.json @@ -0,0 +1,7 @@ +{ + "include_symlinks": true, + "prefixes": [ + "plugins/" + ], + "output": "path-message" +} diff --git a/ansible_collections/community/crypto/tests/sanity/extra/no-unwanted-files.json.license b/ansible_collections/community/crypto/tests/sanity/extra/no-unwanted-files.json.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/extra/no-unwanted-files.json.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/sanity/extra/no-unwanted-files.py b/ansible_collections/community/crypto/tests/sanity/extra/no-unwanted-files.py new file mode 100755 index 000000000..51444ab75 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/extra/no-unwanted-files.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +"""Prevent unwanted files from being added to the source tree.""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import sys + + +def main(): + """Main entry point.""" + paths = sys.argv[1:] or sys.stdin.read().splitlines() + + allowed_extensions = ( + '.cs', + '.ps1', + '.psm1', + '.py', + ) + + skip_paths = set([ + ]) + + skip_directories = ( + ) + + for path in paths: + if path in skip_paths: + continue + + if any(path.startswith(skip_directory) for skip_directory in skip_directories): + continue + + ext = os.path.splitext(path)[1] + + if ext not in allowed_extensions: + print('%s: extension must be one of: %s' % (path, ', '.join(allowed_extensions))) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.10.txt b/ansible_collections/community/crypto/tests/sanity/ignore-2.10.txt new file mode 100644 index 000000000..56340b5b3 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.10.txt @@ -0,0 +1,9 @@ +.azure-pipelines/scripts/publish-codecov.py replace-urlopen +.azure-pipelines/scripts/publish-codecov.py compile-2.6!skip # Uses Python 3.6+ syntax +.azure-pipelines/scripts/publish-codecov.py compile-2.7!skip # Uses Python 3.6+ syntax +.azure-pipelines/scripts/publish-codecov.py compile-3.5!skip # Uses Python 3.6+ syntax +.azure-pipelines/scripts/publish-codecov.py future-import-boilerplate +.azure-pipelines/scripts/publish-codecov.py metaclass-boilerplate +plugins/modules/acme_account_info.py validate-modules:return-syntax-error +tests/ee/roles/smoke/library/smoke_ipaddress.py shebang +tests/ee/roles/smoke/library/smoke_pyyaml.py shebang diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.10.txt.license b/ansible_collections/community/crypto/tests/sanity/ignore-2.10.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.10.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.11.txt b/ansible_collections/community/crypto/tests/sanity/ignore-2.11.txt new file mode 100644 index 000000000..56340b5b3 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.11.txt @@ -0,0 +1,9 @@ +.azure-pipelines/scripts/publish-codecov.py replace-urlopen +.azure-pipelines/scripts/publish-codecov.py compile-2.6!skip # Uses Python 3.6+ syntax +.azure-pipelines/scripts/publish-codecov.py compile-2.7!skip # Uses Python 3.6+ syntax +.azure-pipelines/scripts/publish-codecov.py compile-3.5!skip # Uses Python 3.6+ syntax +.azure-pipelines/scripts/publish-codecov.py future-import-boilerplate +.azure-pipelines/scripts/publish-codecov.py metaclass-boilerplate +plugins/modules/acme_account_info.py validate-modules:return-syntax-error +tests/ee/roles/smoke/library/smoke_ipaddress.py shebang +tests/ee/roles/smoke/library/smoke_pyyaml.py shebang diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.11.txt.license b/ansible_collections/community/crypto/tests/sanity/ignore-2.11.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.11.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.12.txt b/ansible_collections/community/crypto/tests/sanity/ignore-2.12.txt new file mode 100644 index 000000000..c9b09ca4e --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.12.txt @@ -0,0 +1,4 @@ +.azure-pipelines/scripts/publish-codecov.py replace-urlopen +plugins/modules/acme_account_info.py validate-modules:return-syntax-error +tests/ee/roles/smoke/library/smoke_ipaddress.py shebang +tests/ee/roles/smoke/library/smoke_pyyaml.py shebang diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.12.txt.license b/ansible_collections/community/crypto/tests/sanity/ignore-2.12.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.12.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.13.txt b/ansible_collections/community/crypto/tests/sanity/ignore-2.13.txt new file mode 100644 index 000000000..ca127b4fd --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.13.txt @@ -0,0 +1,3 @@ +.azure-pipelines/scripts/publish-codecov.py replace-urlopen +tests/ee/roles/smoke/library/smoke_ipaddress.py shebang +tests/ee/roles/smoke/library/smoke_pyyaml.py shebang diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.13.txt.license b/ansible_collections/community/crypto/tests/sanity/ignore-2.13.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.13.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.14.txt b/ansible_collections/community/crypto/tests/sanity/ignore-2.14.txt new file mode 100644 index 000000000..ca127b4fd --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.14.txt @@ -0,0 +1,3 @@ +.azure-pipelines/scripts/publish-codecov.py replace-urlopen +tests/ee/roles/smoke/library/smoke_ipaddress.py shebang +tests/ee/roles/smoke/library/smoke_pyyaml.py shebang diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.14.txt.license b/ansible_collections/community/crypto/tests/sanity/ignore-2.14.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.14.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.15.txt b/ansible_collections/community/crypto/tests/sanity/ignore-2.15.txt new file mode 100644 index 000000000..ca127b4fd --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.15.txt @@ -0,0 +1,3 @@ +.azure-pipelines/scripts/publish-codecov.py replace-urlopen +tests/ee/roles/smoke/library/smoke_ipaddress.py shebang +tests/ee/roles/smoke/library/smoke_pyyaml.py shebang diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.15.txt.license b/ansible_collections/community/crypto/tests/sanity/ignore-2.15.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.15.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.16.txt b/ansible_collections/community/crypto/tests/sanity/ignore-2.16.txt new file mode 100644 index 000000000..ca127b4fd --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.16.txt @@ -0,0 +1,3 @@ +.azure-pipelines/scripts/publish-codecov.py replace-urlopen +tests/ee/roles/smoke/library/smoke_ipaddress.py shebang +tests/ee/roles/smoke/library/smoke_pyyaml.py shebang diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.16.txt.license b/ansible_collections/community/crypto/tests/sanity/ignore-2.16.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.16.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.9.txt b/ansible_collections/community/crypto/tests/sanity/ignore-2.9.txt new file mode 100644 index 000000000..ce2f4b667 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.9.txt @@ -0,0 +1,8 @@ +.azure-pipelines/scripts/publish-codecov.py replace-urlopen +.azure-pipelines/scripts/publish-codecov.py compile-2.6!skip # Uses Python 3.6+ syntax +.azure-pipelines/scripts/publish-codecov.py compile-2.7!skip # Uses Python 3.6+ syntax +.azure-pipelines/scripts/publish-codecov.py compile-3.5!skip # Uses Python 3.6+ syntax +.azure-pipelines/scripts/publish-codecov.py future-import-boilerplate +.azure-pipelines/scripts/publish-codecov.py metaclass-boilerplate +tests/ee/roles/smoke/library/smoke_ipaddress.py shebang +tests/ee/roles/smoke/library/smoke_pyyaml.py shebang diff --git a/ansible_collections/community/crypto/tests/sanity/ignore-2.9.txt.license b/ansible_collections/community/crypto/tests/sanity/ignore-2.9.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/sanity/ignore-2.9.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/unit/compat/__init__.py b/ansible_collections/community/crypto/tests/unit/compat/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/compat/__init__.py diff --git a/ansible_collections/community/crypto/tests/unit/compat/builtins.py b/ansible_collections/community/crypto/tests/unit/compat/builtins.py new file mode 100644 index 000000000..d548601d4 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/compat/builtins.py @@ -0,0 +1,20 @@ +# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +# +# Compat for python2.7 +# + +# One unittest needs to import builtins via __import__() so we need to have +# the string that represents it +try: + import __builtin__ # noqa: F401, pylint: disable=unused-import +except ImportError: + BUILTINS = 'builtins' +else: + BUILTINS = '__builtin__' diff --git a/ansible_collections/community/crypto/tests/unit/compat/mock.py b/ansible_collections/community/crypto/tests/unit/compat/mock.py new file mode 100644 index 000000000..6ef80a7cb --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/compat/mock.py @@ -0,0 +1,30 @@ +# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +''' +Compat module for Python3.x's unittest.mock module +''' +import sys # noqa: F401, pylint: disable=unused-import + +# Python 2.7 + +# Note: Could use the pypi mock library on python3.x as well as python2.x. It +# is the same as the python3 stdlib mock library + +try: + # Allow wildcard import because we really do want to import all of mock's + # symbols into this compat shim + # pylint: disable=wildcard-import,unused-wildcard-import + from unittest.mock import * # noqa: F401, pylint: disable=unused-import +except ImportError: + # Python 2 + # pylint: disable=wildcard-import,unused-wildcard-import + try: + from mock import * # noqa: F401, pylint: disable=unused-import + except ImportError: + print('You need the mock library installed on python2.x to run tests') diff --git a/ansible_collections/community/crypto/tests/unit/compat/unittest.py b/ansible_collections/community/crypto/tests/unit/compat/unittest.py new file mode 100644 index 000000000..d50bab86f --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/compat/unittest.py @@ -0,0 +1,25 @@ +# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +''' +Compat module for Python2.7's unittest module +''' + +import sys + +# Allow wildcard import because we really do want to import all of +# unittests's symbols into this compat shim +# pylint: disable=wildcard-import,unused-wildcard-import +if sys.version_info < (2, 7): + try: + # Need unittest2 on python2.6 + from unittest2 import * # noqa: F401, pylint: disable=unused-import + except ImportError: + print('You need unittest2 installed on python2.6.x to run tests') +else: + from unittest import * # noqa: F401, pylint: disable=unused-import diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/backend_data.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/backend_data.py new file mode 100644 index 000000000..988bcdaeb --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/backend_data.py @@ -0,0 +1,108 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import base64 +import datetime +import os + +from ansible_collections.community.crypto.plugins.module_utils.acme.backends import ( + CryptoBackend, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + BackendException, +) + + +def load_fixture(name): + with open(os.path.join(os.path.dirname(__file__), 'fixtures', name)) as f: + return f.read() + + +TEST_PEM_DERS = [ + ( + load_fixture('privatekey_1.pem'), + base64.b64decode('MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAo' + 'GCCqGSM49AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3' + 'lWszrS68G8QSzhXR6AmQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw==') + ) +] + + +TEST_KEYS = [ + ( + load_fixture('privatekey_1.pem'), + { + 'alg': 'ES256', + 'hash': 'sha256', + 'jwk': { + 'crv': 'P-256', + 'kty': 'EC', + 'x': 'AJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3lWszrS68E', + 'y': 'vEEs4V0egJkNyM2Q4pp001zu14VcpQ0_Ei8xOOPxKZs', + }, + 'point_size': 32, + 'type': 'ec', + }, + load_fixture('privatekey_1.txt'), + ) +] + + +TEST_CSRS = [ + ( + load_fixture('csr_1.pem'), + set([ + ('dns', 'ansible.com'), + ('dns', 'example.com'), + ('dns', 'example.org') + ]), + load_fixture('csr_1.txt'), + ), + ( + load_fixture('csr_2.pem'), + set([ + ('dns', 'ansible.com'), + ('ip', '127.0.0.1'), + ('ip', '::1'), + ('ip', '2001:d88:ac10:fe01::'), + ('ip', '2001:1234:5678:abcd:9876:5432:10fe:dcba') + ]), + load_fixture('csr_2.txt'), + ), +] + + +TEST_CERT = load_fixture("cert_1.pem") + + +TEST_CERT_DAYS = [ + (datetime.datetime(2018, 11, 15, 1, 2, 3), 11), + (datetime.datetime(2018, 11, 25, 15, 20, 0), 1), + (datetime.datetime(2018, 11, 25, 15, 30, 0), 0), +] + + +class FakeBackend(CryptoBackend): + def parse_key(self, key_file=None, key_content=None, passphrase=None): + raise BackendException('Not implemented in fake backend') + + def sign(self, payload64, protected64, key_data): + raise BackendException('Not implemented in fake backend') + + def create_mac_key(self, alg, key): + raise BackendException('Not implemented in fake backend') + + def get_csr_identifiers(self, csr_filename=None, csr_content=None): + raise BackendException('Not implemented in fake backend') + + def get_cert_days(self, cert_filename=None, cert_content=None, now=None): + raise BackendException('Not implemented in fake backend') + + def create_chain_matcher(self, criterium): + raise BackendException('Not implemented in fake backend') diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.pem b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.pem new file mode 100644 index 000000000..bb4aca519 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBljCCATugAwIBAgIBATAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwthbnNpYmxl +LmNvbTAeFw0xODExMjUxNTI4MjNaFw0xODExMjYxNTI4MjRaMBYxFDASBgNVBAMT +C2Fuc2libGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAJz0yAAXAwEm +OhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6AmQ3IzZDimnTTXO7XhVylDT8S +LzE44/Epm6N6MHgwIwYDVR0RBBwwGoILZXhhbXBsZS5jb22CC2V4YW1wbGUub3Jn +MAwGA1UdEwEB/wQCMAAwDwYDVR0PAQH/BAUDAweAADATBgNVHSUEDDAKBggrBgEF +BQcDATAdBgNVHQ4EFgQUmNL9PMzNaUX74owwLFRiGDS3B3MwCgYIKoZIzj0EAwID +SQAwRgIhALz7Ur96ky0OfM5D9MwFmCg2jccqm/UglGI9+4KeOEIyAiEAwFX4tdll +QSrd1HY/jMsHwdK5wH3JkK/9+fGwyRP11VI= +-----END CERTIFICATE----- diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.pem.license b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.pem.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.pem.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem new file mode 100644 index 000000000..35cf59ccc --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBJTCBzQIBADAWMRQwEgYDVQQDEwthbnNpYmxlLmNvbTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABACc9MgAFwMBJjoU0ZI18cIHnW1juoKG2DN5VrM60uvBvEEs +4V0egJkNyM2Q4pp001zu14VcpQ0/Ei8xOOPxKZugVTBTBgkqhkiG9w0BCQ4xRjBE +MCMGA1UdEQQcMBqCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZzAMBgNVHRMBAf8E +AjAAMA8GA1UdDwEB/wQFAwMHgAAwCgYIKoZIzj0EAwIDRwAwRAIgcDyoRmwFVBDl +FvbFZtiSd5wmJU1ltM6JtcfnLWnjY54CICruOByrropFUkOKKb4xXOYsgaDT93Wr +URnCJfTLr2T3 +-----END CERTIFICATE REQUEST----- diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem.license b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem.old b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem.old new file mode 100644 index 000000000..93da05d60 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem.old @@ -0,0 +1,12 @@ +cryptography 35.0.0 does not support the 'NEW' in there; so to fix tests we removed it. +Once cryptography is fixed we should revert to the old version. + +-----BEGIN NEW CERTIFICATE REQUEST----- +MIIBJTCBzQIBADAWMRQwEgYDVQQDEwthbnNpYmxlLmNvbTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABACc9MgAFwMBJjoU0ZI18cIHnW1juoKG2DN5VrM60uvBvEEs +4V0egJkNyM2Q4pp001zu14VcpQ0/Ei8xOOPxKZugVTBTBgkqhkiG9w0BCQ4xRjBE +MCMGA1UdEQQcMBqCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZzAMBgNVHRMBAf8E +AjAAMA8GA1UdDwEB/wQFAwMHgAAwCgYIKoZIzj0EAwIDRwAwRAIgcDyoRmwFVBDl +FvbFZtiSd5wmJU1ltM6JtcfnLWnjY54CICruOByrropFUkOKKb4xXOYsgaDT93Wr +URnCJfTLr2T3 +-----END NEW CERTIFICATE REQUEST----- diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem.old.license b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem.old.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.pem.old.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.txt b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.txt new file mode 100644 index 000000000..37c5cbda7 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.txt @@ -0,0 +1,28 @@ +Certificate Request: + Data: + Version: 1 (0x0) + Subject: CN = ansible.com + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:00:9c:f4:c8:00:17:03:01:26:3a:14:d1:92:35: + f1:c2:07:9d:6d:63:ba:82:86:d8:33:79:56:b3:3a: + d2:eb:c1:bc:41:2c:e1:5d:1e:80:99:0d:c8:cd:90: + e2:9a:74:d3:5c:ee:d7:85:5c:a5:0d:3f:12:2f:31: + 38:e3:f1:29:9b + ASN1 OID: prime256v1 + NIST CURVE: P-256 + Attributes: + Requested Extensions: + X509v3 Subject Alternative Name: + DNS:example.com, DNS:example.org + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Key Usage: critical + Digital Signature + Signature Algorithm: ecdsa-with-SHA256 + 30:44:02:20:70:3c:a8:46:6c:05:54:10:e5:16:f6:c5:66:d8: + 92:77:9c:26:25:4d:65:b4:ce:89:b5:c7:e7:2d:69:e3:63:9e: + 02:20:2a:ee:38:1c:ab:ae:8a:45:52:43:8a:29:be:31:5c:e6: + 2c:81:a0:d3:f7:75:ab:51:19:c2:25:f4:cb:af:64:f7 diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.txt.license b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_1.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_2.pem b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_2.pem new file mode 100644 index 000000000..295a26e1c --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_2.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEqjCCApICAQAwADCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANv1 +V7gDsh76O//d9wclBcW6kNpWeR6eAggzThwbMZjcO7GFHQsBZCZGGVdyS37uhejc +RrIBdtDDWXhoh3Dz+GQxD+6GuwAEFyL1F3MfT0v1HHoO8fE74G5mD6+ZA2HRDeU9 +jf8BPyVWHBtNbCmJGSlSNOFejWCmwvsLARQxqFBuTyRjgos4BkLyWMqZRukrzO1P +z7IBhuFrB608t+AG4vGnPXZNM7xefhzO8bPOiepT0YS2ERPkFmOy97SnwTGdKykw +ZYM9oKukYhE4Z+yOaTFpJMBNXwDCI5TMnhtc6eJrf5sOFH92n2E9+YWMoahUOiTw +G6XV5HfSpySpwORUaTITQRsPAM+bmK9f1jB6ctfFVwpa8uW/h8pSgbHgZvkeD6s6 +rFLh9TQ24t0vrRmhnY7/AMFgbgJoBTBq0l0lEXS4FCGKDGqQOqSws+eHR/pHA4uY +v8d498SQl9fYsT/c7Uj3/JnMSRVN942yQUFCzwLf0/WzWCi2HTqPM8CPh5ryiJ30 +GAN2eb026/noyTOXm479Tg9o86Tw9qczE0j0CdcRnr6J337RGHQg58PZ7j+hnUmK +wgyclyvjE10ZFBgToMGSnzYp5UeRcOFZ3bnK6LOsGC75mIvz2OQgSQeO5VQASEnO +9uhygNyo91sK4BtVroloit8ZCa82LlsHSCj/mMzPAgMBAAGgZTBjBgkqhkiG9w0B +CQ4xVjBUMFIGA1UdEQRLMEmCC2Fuc2libGUuY29thwR/AAABhxAAAAAAAAAAAAAA +AAAAAAABhxAgAQ2IrBD+AQAAAAAAAAAAhxAgARI0VnirzZh2VDIQ/ty6MA0GCSqG +SIb3DQEBCwUAA4ICAQBFRuANzVRcze+iur0YevjtYIXDa03GoWWkgnLuE8u8epTM +2248duG3TmvVvxWPN4iFrvFcZIvNsevBo+Z7kXJ24m3YldtXvwfAYmCZ062apSoh +yzgo3Q0KfDehwLcoJPe5bh+jbbgJVGGvJug/QFyHSVl+iGyFUXE7pwafl9LuNDi3 +yfOYZLIQ34mBH4Rsvymj9xSTYliWDEEU/o7RrrZeEqkOxNeLh64LbnifdrYUputz +yBURg2xs9hpAsytZJX90iJW8aYPM1aQ7eetqTViIRoqUAmIQobnKlNnpOliBHl+p +RY+AtTnsfAetKUP7OsAZkHRTGAXx0JHJQ1ITY8w5Dcw/v1bDCbAfkDubBP3X+us9 +RQk2h6m74hWFFNu9xOfkNejPf7h4gywfDjo/wGZFSWKyi6avB9V53znZgRUwc009 +p5MM9e37MH8pyBqfnbSwOj4hUoyecRCIAFdywjMb9akP2u15XP3MOtJOEvecyCxN +TZBxupTg65zB47GeSAufnc8FaTZkE8xPuCtbvqOVOkWYqzlqNdCfK8f3AZdlpwLh +38wdUm5G7LIu6aQNiY66aQs9qVpoGvqdmxHRkuSwqwZxGgzcY1yJaWGXQ6R4jgC3 +VKlMTUVs1WYV6jrYLHcVt6Rn/2FVTOns3Jn6cTPOdKViYoqF+yW8yCEAqAskZw== +-----END CERTIFICATE REQUEST----- diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_2.pem.license b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_2.pem.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_2.pem.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_2.txt b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_2.txt new file mode 100644 index 000000000..7a54ee3fa --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_2.txt @@ -0,0 +1,78 @@ +Certificate Request: + Data: + Version: 1 (0x0) + Subject: + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (4096 bit) + Modulus: + 00:db:f5:57:b8:03:b2:1e:fa:3b:ff:dd:f7:07:25: + 05:c5:ba:90:da:56:79:1e:9e:02:08:33:4e:1c:1b: + 31:98:dc:3b:b1:85:1d:0b:01:64:26:46:19:57:72: + 4b:7e:ee:85:e8:dc:46:b2:01:76:d0:c3:59:78:68: + 87:70:f3:f8:64:31:0f:ee:86:bb:00:04:17:22:f5: + 17:73:1f:4f:4b:f5:1c:7a:0e:f1:f1:3b:e0:6e:66: + 0f:af:99:03:61:d1:0d:e5:3d:8d:ff:01:3f:25:56: + 1c:1b:4d:6c:29:89:19:29:52:34:e1:5e:8d:60:a6: + c2:fb:0b:01:14:31:a8:50:6e:4f:24:63:82:8b:38: + 06:42:f2:58:ca:99:46:e9:2b:cc:ed:4f:cf:b2:01: + 86:e1:6b:07:ad:3c:b7:e0:06:e2:f1:a7:3d:76:4d: + 33:bc:5e:7e:1c:ce:f1:b3:ce:89:ea:53:d1:84:b6: + 11:13:e4:16:63:b2:f7:b4:a7:c1:31:9d:2b:29:30: + 65:83:3d:a0:ab:a4:62:11:38:67:ec:8e:69:31:69: + 24:c0:4d:5f:00:c2:23:94:cc:9e:1b:5c:e9:e2:6b: + 7f:9b:0e:14:7f:76:9f:61:3d:f9:85:8c:a1:a8:54: + 3a:24:f0:1b:a5:d5:e4:77:d2:a7:24:a9:c0:e4:54: + 69:32:13:41:1b:0f:00:cf:9b:98:af:5f:d6:30:7a: + 72:d7:c5:57:0a:5a:f2:e5:bf:87:ca:52:81:b1:e0: + 66:f9:1e:0f:ab:3a:ac:52:e1:f5:34:36:e2:dd:2f: + ad:19:a1:9d:8e:ff:00:c1:60:6e:02:68:05:30:6a: + d2:5d:25:11:74:b8:14:21:8a:0c:6a:90:3a:a4:b0: + b3:e7:87:47:fa:47:03:8b:98:bf:c7:78:f7:c4:90: + 97:d7:d8:b1:3f:dc:ed:48:f7:fc:99:cc:49:15:4d: + f7:8d:b2:41:41:42:cf:02:df:d3:f5:b3:58:28:b6: + 1d:3a:8f:33:c0:8f:87:9a:f2:88:9d:f4:18:03:76: + 79:bd:36:eb:f9:e8:c9:33:97:9b:8e:fd:4e:0f:68: + f3:a4:f0:f6:a7:33:13:48:f4:09:d7:11:9e:be:89: + df:7e:d1:18:74:20:e7:c3:d9:ee:3f:a1:9d:49:8a: + c2:0c:9c:97:2b:e3:13:5d:19:14:18:13:a0:c1:92: + 9f:36:29:e5:47:91:70:e1:59:dd:b9:ca:e8:b3:ac: + 18:2e:f9:98:8b:f3:d8:e4:20:49:07:8e:e5:54:00: + 48:49:ce:f6:e8:72:80:dc:a8:f7:5b:0a:e0:1b:55: + ae:89:68:8a:df:19:09:af:36:2e:5b:07:48:28:ff: + 98:cc:cf + Exponent: 65537 (0x10001) + Attributes: + Requested Extensions: + X509v3 Subject Alternative Name: + DNS:ansible.com, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:2001:D88:AC10:FE01:0:0:0:0, IP Address:2001:1234:5678:ABCD:9876:5432:10FE:DCBA + Signature Algorithm: sha256WithRSAEncryption + 45:46:e0:0d:cd:54:5c:cd:ef:a2:ba:bd:18:7a:f8:ed:60:85: + c3:6b:4d:c6:a1:65:a4:82:72:ee:13:cb:bc:7a:94:cc:db:6e: + 3c:76:e1:b7:4e:6b:d5:bf:15:8f:37:88:85:ae:f1:5c:64:8b: + cd:b1:eb:c1:a3:e6:7b:91:72:76:e2:6d:d8:95:db:57:bf:07: + c0:62:60:99:d3:ad:9a:a5:2a:21:cb:38:28:dd:0d:0a:7c:37: + a1:c0:b7:28:24:f7:b9:6e:1f:a3:6d:b8:09:54:61:af:26:e8: + 3f:40:5c:87:49:59:7e:88:6c:85:51:71:3b:a7:06:9f:97:d2: + ee:34:38:b7:c9:f3:98:64:b2:10:df:89:81:1f:84:6c:bf:29: + a3:f7:14:93:62:58:96:0c:41:14:fe:8e:d1:ae:b6:5e:12:a9: + 0e:c4:d7:8b:87:ae:0b:6e:78:9f:76:b6:14:a6:eb:73:c8:15: + 11:83:6c:6c:f6:1a:40:b3:2b:59:25:7f:74:88:95:bc:69:83: + cc:d5:a4:3b:79:eb:6a:4d:58:88:46:8a:94:02:62:10:a1:b9: + ca:94:d9:e9:3a:58:81:1e:5f:a9:45:8f:80:b5:39:ec:7c:07: + ad:29:43:fb:3a:c0:19:90:74:53:18:05:f1:d0:91:c9:43:52: + 13:63:cc:39:0d:cc:3f:bf:56:c3:09:b0:1f:90:3b:9b:04:fd: + d7:fa:eb:3d:45:09:36:87:a9:bb:e2:15:85:14:db:bd:c4:e7: + e4:35:e8:cf:7f:b8:78:83:2c:1f:0e:3a:3f:c0:66:45:49:62: + b2:8b:a6:af:07:d5:79:df:39:d9:81:15:30:73:4d:3d:a7:93: + 0c:f5:ed:fb:30:7f:29:c8:1a:9f:9d:b4:b0:3a:3e:21:52:8c: + 9e:71:10:88:00:57:72:c2:33:1b:f5:a9:0f:da:ed:79:5c:fd: + cc:3a:d2:4e:12:f7:9c:c8:2c:4d:4d:90:71:ba:94:e0:eb:9c: + c1:e3:b1:9e:48:0b:9f:9d:cf:05:69:36:64:13:cc:4f:b8:2b: + 5b:be:a3:95:3a:45:98:ab:39:6a:35:d0:9f:2b:c7:f7:01:97: + 65:a7:02:e1:df:cc:1d:52:6e:46:ec:b2:2e:e9:a4:0d:89:8e: + ba:69:0b:3d:a9:5a:68:1a:fa:9d:9b:11:d1:92:e4:b0:ab:06: + 71:1a:0c:dc:63:5c:89:69:61:97:43:a4:78:8e:00:b7:54:a9: + 4c:4d:45:6c:d5:66:15:ea:3a:d8:2c:77:15:b7:a4:67:ff:61: + 55:4c:e9:ec:dc:99:fa:71:33:ce:74:a5:62:62:8a:85:fb:25: + bc:c8:21:00:a8:0b:24:67 diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_2.txt.license b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_2.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/csr_2.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.pem b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.pem new file mode 100644 index 000000000..97209eda7 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAoGCCqGSM49 +AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6A +mQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw== +-----END EC PRIVATE KEY----- diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.pem.license b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.pem.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.pem.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.txt b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.txt new file mode 100644 index 000000000..e25cfd451 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.txt @@ -0,0 +1,14 @@ +read EC key +Private-Key: (256 bit) +priv: + 35:9a:8d:4d:0f:ca:16:0a:7a:e9:5f:cb:f9:6e:36: + d9:00:bd:ee:c3:93:04:34:d5:b5:c9:f7:bc:db:c4: + 1e:ba +pub: + 04:00:9c:f4:c8:00:17:03:01:26:3a:14:d1:92:35: + f1:c2:07:9d:6d:63:ba:82:86:d8:33:79:56:b3:3a: + d2:eb:c1:bc:41:2c:e1:5d:1e:80:99:0d:c8:cd:90: + e2:9a:74:d3:5c:ee:d7:85:5c:a5:0d:3f:12:2f:31: + 38:e3:f1:29:9b +ASN1 OID: prime256v1 +NIST CURVE: P-256 diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.txt.license b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/privatekey_1.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_cryptography.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_cryptography.py new file mode 100644 index 000000000..59da68a3b --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_cryptography.py @@ -0,0 +1,66 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import pytest + +from ansible_collections.community.crypto.tests.unit.compat.mock import MagicMock + + +from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import ( + HAS_CURRENT_CRYPTOGRAPHY, + CryptographyBackend, +) + +from .backend_data import ( + TEST_KEYS, + TEST_CSRS, + TEST_CERT, + TEST_CERT_DAYS, +) + + +if not HAS_CURRENT_CRYPTOGRAPHY: + pytest.skip('cryptography not found') + + +@pytest.mark.parametrize("pem, result, dummy", TEST_KEYS) +def test_eckeyparse_cryptography(pem, result, dummy, tmpdir): + fn = tmpdir / 'test.pem' + fn.write(pem) + module = MagicMock() + backend = CryptographyBackend(module) + key = backend.parse_key(key_file=str(fn)) + key.pop('key_obj') + assert key == result + key = backend.parse_key(key_content=pem) + key.pop('key_obj') + assert key == result + + +@pytest.mark.parametrize("csr, result, openssl_output", TEST_CSRS) +def test_csridentifiers_cryptography(csr, result, openssl_output, tmpdir): + fn = tmpdir / 'test.csr' + fn.write(csr) + module = MagicMock() + backend = CryptographyBackend(module) + identifiers = backend.get_csr_identifiers(csr_filename=str(fn)) + assert identifiers == result + identifiers = backend.get_csr_identifiers(csr_content=csr) + assert identifiers == result + + +@pytest.mark.parametrize("now, expected_days", TEST_CERT_DAYS) +def test_certdays_cryptography(now, expected_days, tmpdir): + fn = tmpdir / 'test-cert.pem' + fn.write(TEST_CERT) + module = MagicMock() + backend = CryptographyBackend(module) + days = backend.get_cert_days(cert_filename=str(fn), now=now) + assert days == expected_days + days = backend.get_cert_days(cert_content=TEST_CERT, now=now) + assert days == expected_days diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py new file mode 100644 index 000000000..dd30cf795 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py @@ -0,0 +1,63 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import pytest + +from ansible_collections.community.crypto.tests.unit.compat.mock import MagicMock + + +from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import ( + OpenSSLCLIBackend, +) + +from .backend_data import ( + TEST_KEYS, + TEST_CSRS, +) + + +TEST_IPS = [ + ("0:0:0:0:0:0:0:1", "::1"), + ("1::0:2", "1::2"), + ("0000:0001:0000:0000:0000:0000:0000:0001", "0:1::1"), + ("0000:0001:0000:0000:0001:0000:0000:0001", "0:1::1:0:0:1"), + ("0000:0001:0000:0001:0000:0001:0000:0001", "0:1:0:1:0:1:0:1"), + ("0.0.0.0", "0.0.0.0"), + ("2001:d88:ac10:fe01:0:0:0:0", "2001:d88:ac10:fe01::"), + ("0000:0000:0000:0000:0000:0000:0000:0000", "::"), +] + + +@pytest.mark.parametrize("pem, result, openssl_output", TEST_KEYS) +def test_eckeyparse_openssl(pem, result, openssl_output, tmpdir): + fn = tmpdir / 'test.key' + fn.write(pem) + module = MagicMock() + module.run_command = MagicMock(return_value=(0, openssl_output, 0)) + backend = OpenSSLCLIBackend(module, openssl_binary='openssl') + key = backend.parse_key(key_file=str(fn)) + key.pop('key_file') + assert key == result + + +@pytest.mark.parametrize("csr, result, openssl_output", TEST_CSRS) +def test_csridentifiers_openssl(csr, result, openssl_output, tmpdir): + fn = tmpdir / 'test.csr' + fn.write(csr) + module = MagicMock() + module.run_command = MagicMock(return_value=(0, openssl_output, 0)) + backend = OpenSSLCLIBackend(module, openssl_binary='openssl') + identifiers = backend.get_csr_identifiers(str(fn)) + assert identifiers == result + + +@pytest.mark.parametrize("ip, result", TEST_IPS) +def test_normalize_ip(ip, result): + module = MagicMock() + backend = OpenSSLCLIBackend(module, openssl_binary='openssl') + assert backend._normalize_ip(ip) == result diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_challenges.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_challenges.py new file mode 100644 index 000000000..18e4118f6 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_challenges.py @@ -0,0 +1,252 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import pytest + +from ansible_collections.community.crypto.tests.unit.compat.mock import MagicMock + + +from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import ( + combine_identifier, + split_identifier, + Challenge, + Authorization, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + ACMEProtocolException, + ModuleFailException, +) + + +def test_combine_identifier(): + assert combine_identifier('', '') == ':' + assert combine_identifier('a', 'b') == 'a:b' + + +def test_split_identifier(): + assert split_identifier(':') == ['', ''] + assert split_identifier('a:b') == ['a', 'b'] + assert split_identifier('a:b:c') == ['a', 'b:c'] + with pytest.raises(ModuleFailException) as exc: + split_identifier('a') + assert exc.value.msg == 'Identifier "a" is not of the form <type>:<identifier>' + + +def test_challenge_from_to_json(): + client = MagicMock() + + data = { + 'url': 'xxx', + 'type': 'type', + 'status': 'valid', + } + client.version = 2 + challenge = Challenge.from_json(client, data) + assert challenge.data == data + assert challenge.type == 'type' + assert challenge.url == 'xxx' + assert challenge.status == 'valid' + assert challenge.token is None + assert challenge.to_json() == data + + data = { + 'type': 'type', + 'status': 'valid', + 'token': 'foo', + } + challenge = Challenge.from_json(None, data, url='xxx') + assert challenge.data == data + assert challenge.type == 'type' + assert challenge.url == 'xxx' + assert challenge.status == 'valid' + assert challenge.token == 'foo' + assert challenge.to_json() == data + + data = { + 'uri': 'xxx', + 'type': 'type', + 'status': 'valid', + } + client.version = 1 + challenge = Challenge.from_json(client, data) + assert challenge.data == data + assert challenge.type == 'type' + assert challenge.url == 'xxx' + assert challenge.status == 'valid' + assert challenge.token is None + assert challenge.to_json() == data + + +def test_authorization_from_to_json(): + client = MagicMock() + client.version = 2 + + data = { + 'challenges': [], + 'status': 'valid', + 'identifier': { + 'type': 'dns', + 'value': 'example.com', + }, + } + authz = Authorization.from_json(client, data, 'xxx') + assert authz.url == 'xxx' + assert authz.status == 'valid' + assert authz.identifier == 'example.com' + assert authz.identifier_type == 'dns' + assert authz.challenges == [] + assert authz.to_json() == { + 'uri': 'xxx', + 'challenges': [], + 'status': 'valid', + 'identifier': { + 'type': 'dns', + 'value': 'example.com', + }, + } + + data = { + 'challenges': [ + { + 'url': 'xxxyyy', + 'type': 'type', + 'status': 'valid', + } + ], + 'status': 'valid', + 'identifier': { + 'type': 'dns', + 'value': 'example.com', + }, + 'wildcard': True, + } + authz = Authorization.from_json(client, data, 'xxx') + assert authz.url == 'xxx' + assert authz.status == 'valid' + assert authz.identifier == '*.example.com' + assert authz.identifier_type == 'dns' + assert len(authz.challenges) == 1 + assert authz.challenges[0].data == { + 'url': 'xxxyyy', + 'type': 'type', + 'status': 'valid', + } + assert authz.to_json() == { + 'uri': 'xxx', + 'challenges': [ + { + 'url': 'xxxyyy', + 'type': 'type', + 'status': 'valid', + } + ], + 'status': 'valid', + 'identifier': { + 'type': 'dns', + 'value': 'example.com', + }, + 'wildcard': True, + } + + client.version = 1 + + data = { + 'challenges': [], + 'identifier': { + 'type': 'dns', + 'value': 'example.com', + }, + } + authz = Authorization.from_json(client, data, 'xxx') + assert authz.url == 'xxx' + assert authz.status == 'pending' + assert authz.identifier == 'example.com' + assert authz.identifier_type == 'dns' + assert authz.challenges == [] + assert authz.to_json() == { + 'uri': 'xxx', + 'challenges': [], + 'identifier': { + 'type': 'dns', + 'value': 'example.com', + }, + } + + +def test_authorization_create_error(): + client = MagicMock() + client.version = 2 + client.directory.directory = {} + with pytest.raises(ACMEProtocolException) as exc: + Authorization.create(client, 'dns', 'example.com') + + assert exc.value.msg == 'ACME endpoint does not support pre-authorization.' + + +def test_wait_for_validation_error(): + client = MagicMock() + client.version = 2 + data = { + 'challenges': [ + { + 'url': 'xxxyyy1', + 'type': 'dns-01', + 'status': 'invalid', + 'error': { + 'type': 'dns-failed', + 'subproblems': [ + { + 'type': 'subproblem', + 'detail': 'example.com DNS-01 validation failed', + }, + ] + }, + }, + { + 'url': 'xxxyyy2', + 'type': 'http-01', + 'status': 'invalid', + 'error': { + 'type': 'http-failed', + 'subproblems': [ + { + 'type': 'subproblem', + 'detail': 'example.com HTTP-01 validation failed', + }, + ] + }, + }, + { + 'url': 'xxxyyy3', + 'type': 'something-else', + 'status': 'valid', + }, + ], + 'status': 'invalid', + 'identifier': { + 'type': 'dns', + 'value': 'example.com', + }, + } + client.get_request = MagicMock(return_value=(data, {})) + authz = Authorization.from_json(client, data, 'xxx') + with pytest.raises(ACMEProtocolException) as exc: + authz.wait_for_validation(client, 'dns') + + assert exc.value.msg == ( + 'Failed to validate challenge for dns:example.com: Status is "invalid". Challenge dns-01: Error dns-failed Subproblems:\n' + '(dns-01.0) Error subproblem: "example.com DNS-01 validation failed"; Challenge http-01: Error http-failed Subproblems:\n' + '(http-01.0) Error subproblem: "example.com HTTP-01 validation failed".' + ) + data = data.copy() + data['uri'] = 'xxx' + assert exc.value.module_fail_args == { + 'identifier': 'dns:example.com', + 'authorization': data, + } diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_errors.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_errors.py new file mode 100644 index 000000000..c84015d33 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_errors.py @@ -0,0 +1,380 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import pytest + +from ansible_collections.community.crypto.tests.unit.compat.mock import MagicMock + + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + format_error_problem, + ACMEProtocolException, +) + + +TEST_FORMAT_ERROR_PROBLEM = [ + ( + { + 'type': 'foo', + }, + '', + 'Error foo' + ), + ( + { + 'type': 'foo', + 'title': 'bar' + }, + '', + 'Error "bar" (foo)' + ), + ( + { + 'type': 'foo', + 'detail': 'bar baz' + }, + '', + 'Error foo: "bar baz"' + ), + ( + { + 'type': 'foo', + 'subproblems': [] + }, + '', + 'Error foo Subproblems:' + ), + ( + { + 'type': 'foo', + 'subproblems': [ + { + 'type': 'bar', + }, + ] + }, + '', + 'Error foo Subproblems:\n(0) Error bar' + ), + ( + { + 'type': 'foo', + 'subproblems': [ + { + 'type': 'bar', + 'subproblems': [ + { + 'type': 'baz', + }, + ] + }, + ] + }, + '', + 'Error foo Subproblems:\n(0) Error bar Subproblems:\n(0.0) Error baz' + ), + ( + { + 'type': 'foo', + 'title': 'Foo Error', + 'detail': 'Foo went wrong', + 'subproblems': [ + { + 'type': 'bar', + 'detail': 'Bar went wrong', + 'subproblems': [ + { + 'type': 'baz', + 'title': 'Baz Error', + }, + ] + }, + { + 'type': 'bar2', + 'title': 'Bar 2 Error', + 'detail': 'Bar really went wrong' + }, + ] + }, + 'X.', + 'Error "Foo Error" (foo): "Foo went wrong" Subproblems:\n' + '(X.0) Error bar: "Bar went wrong" Subproblems:\n' + '(X.0.0) Error "Baz Error" (baz)\n' + '(X.1) Error "Bar 2 Error" (bar2): "Bar really went wrong"' + ), +] + + +@pytest.mark.parametrize("problem, subproblem_prefix, result", TEST_FORMAT_ERROR_PROBLEM) +def test_format_error_problem(problem, subproblem_prefix, result): + res = format_error_problem(problem, subproblem_prefix) + assert res == result + + +def create_regular_response(response_text): + response = MagicMock() + response.read = MagicMock(return_value=response_text.encode('utf-8')) + response.closed = False + return response + + +def create_error_response(): + response = MagicMock() + response.read = MagicMock(side_effect=AttributeError('read')) + response.closed = True + return response + + +def create_decode_error(msg): + def f(content): + raise Exception(msg) + + return f + + +TEST_ACME_PROTOCOL_EXCEPTION = [ + ( + {}, + None, + 'ACME request failed.', + { + }, + ), + ( + { + 'msg': 'Foo', + 'extras': { + 'foo': 'bar', + }, + }, + None, + 'Foo.', + { + 'foo': 'bar', + }, + ), + ( + { + 'info': { + 'url': 'https://ca.example.com/foo', + 'status': 201, + }, + }, + None, + 'ACME request failed for https://ca.example.com/foo with HTTP status 201 Created.', + { + 'http_url': 'https://ca.example.com/foo', + 'http_status': 201, + }, + ), + ( + { + 'info': { + 'url': 'https://ca.example.com/foo', + 'status': 201, + }, + 'response': create_regular_response('xxx'), + }, + None, + 'ACME request failed for https://ca.example.com/foo with HTTP status 201 Created. The raw error result: xxx', + { + 'http_url': 'https://ca.example.com/foo', + 'http_status': 201, + }, + ), + ( + { + 'info': { + 'url': 'https://ca.example.com/foo', + 'status': 201, + }, + 'response': create_regular_response('xxx'), + }, + create_decode_error('yyy'), + 'ACME request failed for https://ca.example.com/foo with HTTP status 201 Created. The raw error result: xxx', + { + 'http_url': 'https://ca.example.com/foo', + 'http_status': 201, + }, + ), + ( + { + 'info': { + 'url': 'https://ca.example.com/foo', + 'status': 201, + }, + 'response': create_regular_response('xxx'), + }, + lambda content: dict(foo='bar'), + "ACME request failed for https://ca.example.com/foo with HTTP status 201 Created. The JSON error result: {'foo': 'bar'}", + { + 'http_url': 'https://ca.example.com/foo', + 'http_status': 201, + }, + ), + ( + { + 'info': { + 'url': 'https://ca.example.com/foo', + 'status': 201, + }, + 'response': create_error_response(), + }, + None, + 'ACME request failed for https://ca.example.com/foo with HTTP status 201 Created.', + { + 'http_url': 'https://ca.example.com/foo', + 'http_status': 201, + }, + ), + ( + { + 'info': { + 'url': 'https://ca.example.com/foo', + 'status': 201, + 'body': 'xxx', + }, + 'response': create_error_response(), + }, + lambda content: dict(foo='bar'), + "ACME request failed for https://ca.example.com/foo with HTTP status 201 Created. The JSON error result: {'foo': 'bar'}", + { + 'http_url': 'https://ca.example.com/foo', + 'http_status': 201, + }, + ), + ( + { + 'info': { + 'url': 'https://ca.example.com/foo', + 'status': 201, + }, + 'content': 'xxx', + }, + None, + "ACME request failed for https://ca.example.com/foo with HTTP status 201 Created. The raw error result: xxx", + { + 'http_url': 'https://ca.example.com/foo', + 'http_status': 201, + }, + ), + ( + { + 'info': { + 'url': 'https://ca.example.com/foo', + 'status': 400, + }, + 'content_json': { + 'foo': 'bar', + }, + 'extras': { + 'bar': 'baz', + } + }, + None, + "ACME request failed for https://ca.example.com/foo with HTTP status 400 Bad Request. The JSON error result: {'foo': 'bar'}", + { + 'http_url': 'https://ca.example.com/foo', + 'http_status': 400, + 'bar': 'baz', + }, + ), + ( + { + 'info': { + 'url': 'https://ca.example.com/foo', + 'status': 201, + }, + 'content_json': { + 'type': 'foo', + }, + }, + None, + "ACME request failed for https://ca.example.com/foo with HTTP status 201 Created. The JSON error result: {'type': 'foo'}", + { + 'http_url': 'https://ca.example.com/foo', + 'http_status': 201, + }, + ), + ( + { + 'info': { + 'url': 'https://ca.example.com/foo', + 'status': 400, + }, + 'content_json': { + 'type': 'foo', + }, + }, + None, + "ACME request failed for https://ca.example.com/foo with status 400 Bad Request. Error foo.", + { + 'http_url': 'https://ca.example.com/foo', + 'http_status': 400, + 'problem': { + 'type': 'foo', + }, + 'subproblems': [], + }, + ), + ( + { + 'info': { + 'url': 'https://ca.example.com/foo', + 'status': 400, + }, + 'content_json': { + 'type': 'foo', + 'title': 'Foo Error', + 'subproblems': [ + { + 'type': 'bar', + 'detail': 'This is a bar error', + 'details': 'Details.', + }, + ], + }, + }, + None, + "ACME request failed for https://ca.example.com/foo with status 400 Bad Request. Error \"Foo Error\" (foo). Subproblems:\n" + "(0) Error bar: \"This is a bar error\".", + { + 'http_url': 'https://ca.example.com/foo', + 'http_status': 400, + 'problem': { + 'type': 'foo', + 'title': 'Foo Error', + }, + 'subproblems': [ + { + 'type': 'bar', + 'detail': 'This is a bar error', + 'details': 'Details.', + }, + ], + }, + ), +] + + +@pytest.mark.parametrize("input, from_json, msg, args", TEST_ACME_PROTOCOL_EXCEPTION) +def test_acme_protocol_exception(input, from_json, msg, args): + if from_json is None: + module = None + else: + module = MagicMock() + module.from_json = from_json + with pytest.raises(ACMEProtocolException) as exc: + raise ACMEProtocolException(module, **input) + + print(exc.value.msg) + print(exc.value.module_fail_args) + print(msg) + print(args) + assert exc.value.msg == msg + assert exc.value.module_fail_args == args diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_io.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_io.py new file mode 100644 index 000000000..818a5fe84 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_io.py @@ -0,0 +1,33 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible_collections.community.crypto.tests.unit.compat.mock import MagicMock + + +from ansible_collections.community.crypto.plugins.module_utils.acme.io import ( + read_file, + write_file, +) + + +TEST_TEXT = r"""1234 +5678""" + + +def test_read_file(tmpdir): + fn = tmpdir / 'test.txt' + fn.write(TEST_TEXT) + assert read_file(str(fn), 't') == TEST_TEXT + assert read_file(str(fn), 'b') == TEST_TEXT.encode('utf-8') + + +def test_write_file(tmpdir): + fn = tmpdir / 'test.txt' + module = MagicMock() + write_file(module, str(fn), TEST_TEXT.encode('utf-8')) + assert fn.read() == TEST_TEXT diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_orders.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_orders.py new file mode 100644 index 000000000..c2139083c --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_orders.py @@ -0,0 +1,59 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import pytest + +from ansible_collections.community.crypto.tests.unit.compat.mock import MagicMock + + +from ansible_collections.community.crypto.plugins.module_utils.acme.orders import ( + Order, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + ACMEProtocolException, +) + + +def test_order_from_json(): + client = MagicMock() + + data = { + 'status': 'valid', + 'identifiers': [], + 'authorizations': [], + } + client.version = 2 + order = Order.from_json(client, data, 'xxx') + assert order.data == data + assert order.url == 'xxx' + assert order.status == 'valid' + assert order.identifiers == [] + assert order.finalize_uri is None + assert order.certificate_uri is None + assert order.authorization_uris == [] + assert order.authorizations == {} + + +def test_wait_for_finalization_error(): + client = MagicMock() + client.version = 2 + + data = { + 'status': 'invalid', + 'identifiers': [], + 'authorizations': [], + } + order = Order.from_json(client, data, 'xxx') + + client.get_request = MagicMock(return_value=(data, {})) + with pytest.raises(ACMEProtocolException) as exc: + order.wait_for_finalization(client) + + assert exc.value.msg.startswith('Failed to wait for order to complete; got status "invalid". The JSON result: ') + assert exc.value.module_fail_args == {} diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_utils.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_utils.py new file mode 100644 index 000000000..9bdd8eb6e --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_utils.py @@ -0,0 +1,39 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import pytest + + +from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( + nopad_b64, + pem_to_der, +) + +from .backend_data import ( + TEST_PEM_DERS, +) + + +NOPAD_B64 = [ + ("", ""), + ("\n", "Cg"), + ("123", "MTIz"), + ("Lorem?ipsum", "TG9yZW0_aXBzdW0"), +] + + +@pytest.mark.parametrize("value, result", NOPAD_B64) +def test_nopad_b64(value, result): + assert nopad_b64(value.encode('utf-8')) == result + + +@pytest.mark.parametrize("pem, der", TEST_PEM_DERS) +def test_pem_to_der(pem, der, tmpdir): + fn = tmpdir / 'test.pem' + fn.write(pem) + assert pem_to_der(str(fn)) == der diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/crypto/test_asn1.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/crypto/test_asn1.py new file mode 100644 index 000000000..ea4b8a5d9 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/crypto/test_asn1.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Jordan Borean <jborean93@gmail.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import base64 +import re +import subprocess + +import pytest + +from ansible_collections.community.crypto.plugins.module_utils.crypto._asn1 import ( + serialize_asn1_string_as_der, + pack_asn1, +) + + +TEST_CASES = [ + ('UTF8:Hello World', b'\x0c\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + + ('EXPLICIT:10,UTF8:Hello World', b'\xaa\x0d\x0c\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + ('EXPLICIT:12U,UTF8:Hello World', b'\x0c\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + ('EXPLICIT:10A,UTF8:Hello World', b'\x6a\x0d\x0c\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + ('EXPLICIT:10P,UTF8:Hello World', b'\xea\x0d\x0c\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + ('EXPLICIT:10C,UTF8:Hello World', b'\xaa\x0d\x0c\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + ('EXPLICIT:1024P,UTF8:Hello World', b'\xff\x88\x00\x0d\x0c\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + + ('IMPLICIT:10,UTF8:Hello World', b'\x8a\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + ('IMPLICIT:12U,UTF8:Hello World', b'\x0c\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + ('IMPLICIT:10A,UTF8:Hello World', b'\x4a\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + ('IMPLICIT:10P,UTF8:Hello World', b'\xca\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + ('IMPLICIT:10C,UTF8:Hello World', b'\x8a\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + ('IMPLICIT:1024P,UTF8:Hello World', b'\xdf\x88\x00\x0b\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64'), + + # Tests large data lengths, special logic for the length octet encoding. + ('UTF8:' + ('A' * 600), b'\x0c\x82\x02\x58' + (b'\x41' * 600)), + + # This isn't valid with openssl asn1parse but has been validated against an ASN.1 parser. OpenSSL seems to read the + # data u"café" encoded as UTF-8 bytes b"caf\xc3\xa9", decodes that internally with latin-1 (or similar variant) as + # u"café" then encodes that to UTF-8 b"caf\xc3\x83\xc2\xa9" for the UTF8String. Ultimately openssl is wrong here + # so we keep our assertion happening. + (u'UTF8:café', b'\x0c\x05\x63\x61\x66\xc3\xa9'), +] + + +@pytest.mark.parametrize('value, expected', TEST_CASES) +def test_serialize_asn1_string_as_der(value, expected): + actual = serialize_asn1_string_as_der(value) + print("%s | %s" % (value, base64.b16encode(actual).decode())) + assert actual == expected + + +@pytest.mark.parametrize('value', [ + 'invalid', + 'EXPLICIT,UTF:value', +]) +def test_serialize_asn1_string_as_der_invalid_format(value): + expected = "The ASN.1 serialized string must be in the format [modifier,]type[:value]" + with pytest.raises(ValueError, match=re.escape(expected)): + serialize_asn1_string_as_der(value) + + +def test_serialize_asn1_string_as_der_invalid_type(): + expected = "The ASN.1 serialized string is not a known type \"OID\", only UTF8 types are supported" + with pytest.raises(ValueError, match=re.escape(expected)): + serialize_asn1_string_as_der("OID:1.2.3.4") + + +def test_pack_asn_invalid_class(): + with pytest.raises(ValueError, match="tag_class must be between 0 and 3 not 4"): + pack_asn1(4, True, 0, b"") + + +@pytest.mark.skip() # This is to just to build the test case assertions and shouldn't run normally. +@pytest.mark.parametrize('value, expected', TEST_CASES) +def test_test_cases(value, expected, tmp_path): + test_file = tmp_path / 'test.der' + subprocess.run(['openssl', 'asn1parse', '-genstr', value, '-noout', '-out', test_file], check=True) + + with open(test_file, mode='rb') as fd: + b_data = fd.read() + + hex_str = base64.b16encode(b_data).decode().lower() + print("%s | \\x%s" % (value, "\\x".join([hex_str[i:i + 2] for i in range(0, len(hex_str), 2)]))) + + # This is a know edge case where openssl asn1parse does not work properly. + if value != u'UTF8:café': + assert b_data == expected diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/crypto/test_cryptography_support.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/crypto/test_cryptography_support.py new file mode 100644 index 000000000..10f27d239 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/crypto/test_cryptography_support.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Jordan Borean <jborean93@gmail.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import re + +import cryptography +import pytest + +from cryptography.x509 import NameAttribute, oid + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_get_name, + _adjust_idn, + _parse_dn_component, + _parse_dn, +) + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + + +@pytest.mark.parametrize('unicode, idna, cycled_unicode', [ + (u'..', u'..', None), + (u'foo.com', u'foo.com', None), + (u'.foo.com.', u'.foo.com.', None), + (u'*.foo.com', u'*.foo.com', None), + (u'straße', u'xn--strae-oqa', None), + (u'ffóò.ḃâŗ.çøṁ', u'xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n', u'ffóò.ḃâŗ.çøṁ'), + (u'*.☺.', u'*.xn--74h.', None), +]) +def test_adjust_idn(unicode, idna, cycled_unicode): + if cycled_unicode is None: + cycled_unicode = unicode + + result = _adjust_idn(unicode, 'ignore') + print(result, unicode) + assert result == unicode + + result = _adjust_idn(idna, 'ignore') + print(result, idna) + assert result == idna + + result = _adjust_idn(unicode, 'unicode') + print(result, unicode) + assert result == unicode + + result = _adjust_idn(idna, 'unicode') + print(result, cycled_unicode) + assert result == cycled_unicode + + result = _adjust_idn(unicode, 'idna') + print(result, idna) + assert result == idna + + result = _adjust_idn(idna, 'idna') + print(result, idna) + assert result == idna + + +@pytest.mark.parametrize('value, idn_rewrite, message', [ + (u'bar', 'foo', re.escape(u'Invalid value for idn_rewrite: "foo"')), +]) +def test_adjust_idn_fail_valueerror(value, idn_rewrite, message): + with pytest.raises(ValueError, match=message): + result = _adjust_idn(value, idn_rewrite) + + +@pytest.mark.parametrize('value, idn_rewrite, message', [ + ( + u'xn--a', + 'unicode', + u'''^Error while transforming part u?"xn\\-\\-a" of IDNA DNS name u?"xn\\-\\-a" to Unicode\\.''' + u''' IDNA2008 transformation resulted in "Codepoint U\\+0080 at position 1 of u?'\\\\x80' not allowed",''' + u''' IDNA2003 transformation resulted in "(decoding with 'idna' codec failed''' + u''' \\(UnicodeError: )?Invalid character u?'\\\\x80'\\)?"\\.$''' + ), +]) +def test_adjust_idn_fail_user_error(value, idn_rewrite, message): + with pytest.raises(OpenSSLObjectError, match=message): + result = _adjust_idn(value, idn_rewrite) + + +def test_cryptography_get_name_invalid_prefix(): + with pytest.raises(OpenSSLObjectError, match="^Cannot parse Subject Alternative Name"): + cryptography_get_name('fake:value') + + +def test_cryptography_get_name_other_name_no_oid(): + with pytest.raises(OpenSSLObjectError, match="Cannot parse Subject Alternative Name otherName"): + cryptography_get_name('otherName:value') + + +def test_cryptography_get_name_other_name_utfstring(): + actual = cryptography_get_name('otherName:1.3.6.1.4.1.311.20.2.3;UTF8:Hello World') + assert actual.type_id.dotted_string == '1.3.6.1.4.1.311.20.2.3' + assert actual.value == b'\x0c\x0bHello World' + + +@pytest.mark.parametrize('name, options, expected', [ + (b'CN=x ', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u'x '), b'')), + (b'CN=\\ ', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u' '), b'')), + (b'CN=\\#', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u'#'), b'')), + (b'CN=#402032', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u'@ 2'), b'')), + (b'CN = x ', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u'x '), b'')), + (b'CN = x\\, ', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u'x, '), b'')), + (b'CN = x\\40 ', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u'x@ '), b'')), + (b'CN = \\ , / ', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u' '), b', / ')), + (b'CN = \\ , / ', {'sep': b'/'}, (NameAttribute(oid.NameOID.COMMON_NAME, u' , '), b'/ ')), + (b'CN = \\ , / ', {'decode_remainder': False}, (NameAttribute(oid.NameOID.COMMON_NAME, u'\\ , / '), b'')), + # Some examples from https://datatracker.ietf.org/doc/html/rfc4514#section-4: + (b'CN=James \\"Jim\\" Smith\\, III', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u'James "Jim" Smith, III'), b'')), + (b'CN=Before\\0dAfter', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u'Before\x0dAfter'), b'')), + (b'1.3.6.1.4.1.1466.0=#04024869', {}, (NameAttribute(oid.ObjectIdentifier(u'1.3.6.1.4.1.1466.0'), u'\x04\x02Hi'), b'')), + (b'CN=Lu\\C4\\8Di\\C4\\87', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u'Lučić'), b'')), +]) +def test_parse_dn_component(name, options, expected): + result = _parse_dn_component(name, **options) + print(result, expected) + assert result == expected + + +# Cryptography < 2.9 does not allow empty strings +# (https://github.com/pyca/cryptography/commit/87b2749c52e688c809f1861e55d958c64147493c) +if LooseVersion(cryptography.__version__) >= LooseVersion('2.9'): + @pytest.mark.parametrize('name, options, expected', [ + (b'CN=', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u''), b'')), + (b'CN= ', {}, (NameAttribute(oid.NameOID.COMMON_NAME, u''), b'')), + ]) + def test_parse_dn_component_not_py26(name, options, expected): + result = _parse_dn_component(name, **options) + print(result, expected) + assert result == expected + + +@pytest.mark.parametrize('name, options, message', [ + (b'CN=\\0', {}, u'Hex escape sequence "\\0" incomplete at end of string'), + (b'CN=\\0,', {}, u'Hex escape sequence "\\0," has invalid second letter'), + (b'CN=#0,', {}, u'Invalid hex sequence entry "0,"'), +]) +def test_parse_dn_component_failure(name, options, message): + with pytest.raises(OpenSSLObjectError, match=u'^%s$' % re.escape(message)): + result = _parse_dn_component(name, **options) + + +@pytest.mark.parametrize('name, expected', [ + (b'CN=foo', [NameAttribute(oid.NameOID.COMMON_NAME, u'foo')]), + (b'CN=foo,CN=bar', [NameAttribute(oid.NameOID.COMMON_NAME, u'foo'), NameAttribute(oid.NameOID.COMMON_NAME, u'bar')]), + (b'CN = foo , CN = bar', [NameAttribute(oid.NameOID.COMMON_NAME, u'foo '), NameAttribute(oid.NameOID.COMMON_NAME, u'bar')]), +]) +def test_parse_dn(name, expected): + result = _parse_dn(name) + print(result, expected) + assert result == expected + + +@pytest.mark.parametrize('name, message', [ + (b'CN=\\0', u'Error while parsing distinguished name "CN=\\0": Hex escape sequence "\\0" incomplete at end of string'), + (b'CN=x,', u'Error while parsing distinguished name "CN=x,": unexpected end of string'), +]) +def test_parse_dn_failure(name, message): + with pytest.raises(OpenSSLObjectError, match=u'^%s$' % re.escape(message)): + result = _parse_dn(name) diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/openssh/test_certificate.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/openssh/test_certificate.py new file mode 100644 index 000000000..390a9626a --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/openssh/test_certificate.py @@ -0,0 +1,350 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +from ansible_collections.community.crypto.plugins.module_utils.openssh.certificate import ( + OpensshCertificate, + OpensshCertificateOption, + OpensshCertificateTimeParameters, + parse_option_list +) + +# Type: ssh-rsa-cert-v01@openssh.com user certificate +# Public key: RSA-CERT SHA256:SvUwwUer4AwsdePYseJR3LcZS8lnKi6BqiL51Dop030 +# Signing CA: DSA SHA256:YCdJ2lYU+FSkWUud7zg1SJszprXoRGNU/GVcqXUjgC8 +# Key ID: "test" +# Serial: 0 +# Valid: forever +# Principals: (none) +# Critical Options: (none) +# Extensions: +# permit-X11-forwarding +# permit-agent-forwarding +# permit-port-forwarding +# permit-pty +# permit-user-rc +RSA_CERT_SIGNED_BY_DSA = ( + b'ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgY9CvhGpyvBB611Lmx6hHPD+CmeJ0oW' + + b'SSK1q6K3h5CS4AAAADAQABAAABAQDKYIJtpFaWpTNNifmuV3DM9BBdngMG28jWPy4C/SoZg4EP7mkYUsG6hN+LgjOL17YEF7bKDEWPl9sQS' + + b'92iD+AuAPrjnHVQ9VG5hbTYiQAaicj6hxqBoNqGQWxDzhZL4B35MgqmoUOBGnzYA/fKgqhRVzOXbWFxKLtzSJzB+Z+kmeoBzq+4MazL4Bko' + + b'yPZMrIMnvxiluv+kqE9SWeJ/5e7WXdtbYTnSR4WN3gW/BMKEoKQk/UGwuPvCiRq+y8LorJP4B1Wfwlm/meqtbTidXyCcQPR9xWpce3rRjLL' + + b'T6cimUjWrbx7Q1SlsypdkclgPSTu9Jg457am8tnQUgnL7VdetAAAAAAAAAAAAAAABAAAABHRlc3QAAAAAAAAAAAAAAAD//////////wAAAA' + + b'AAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0L' + + b'WZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAGxAAAAB3NzaC1kc3MAAACBAPV/' + + b'b5FknU8e56TWAGLRQ0v3c3f5jAS0txcwqtYLHLulTqyMcLL0MyzWxXv77MpjTMwEjWXLbfNWdk/qmsjfBynzs2nSZ7clVsqt/ZOadcBFEhq' + + b'ZM0l+1ZCPkhQiqsD2aodGbkVcJgqL5Z5krzB5MTey7c8rlAAxKOjfs70Bg8MPAAAAFQCW466dSEu2Pf0u8AA5SHgH0i/xuwAAAIBc23gfmv' + + b'GC+oaUAXiak17kH6NvOSJXZBdk/8CyGK6yL+CHKrKyffe6BbiVXwC6sUIa9j4YsFeyYwPFGBtfLuNUmgyKYTJcCM2zJLBykmTIvjSdRaYGN' + + b'Rkyi8GnzVV2lWxQ+4m4UGeTPbPN/OG4B0NwDbBJGbVJv0xJPq2EBKoUdgAAAIAyrFxGDLtOZFZ2fgONVaKaapEpJ5f3qPhLDXxVQ/BKVUkU' + + b'RA4AHHyXF2AMiiOOiHLrO5xsEGUyW+OISFm+6m17cEPNixA7G1fBniLvyVv2woyYW3kaY4J9z266kAFzFWVNgwr+T7MY0hEvct8VFA97JMR' + + b'Q7c8c/tNDaL7uqV46QQAAADcAAAAHc3NoLWRzcwAAAChaQ94wqca+KhkHtbkLpjvGsfu0Gy03SAb0+o11Shk/BXnK7N/cwEVD ' + + b'ansible@ansible-host' +) +RSA_FINGERPRINT = 'SHA256:SvUwwUer4AwsdePYseJR3LcZS8lnKi6BqiL51Dop030' +# Type: ssh-dss-cert-v01@openssh.com user certificate +# Public key: DSA-CERT SHA256:YCdJ2lYU+FSkWUud7zg1SJszprXoRGNU/GVcqXUjgC8 +# Signing CA: ECDSA SHA256:w9lp4zGRJShhm4DzO3ulVm0BEcR0PMjrM6VanQo4C0w +# Key ID: "test" +# Serial: 0 +# Valid: forever +# Principals: (none) +# Critical Options: (none) +# Extensions: (none) +DSA_CERT_SIGNED_BY_ECDSA_NO_OPTS = ( + b'ssh-dss-cert-v01@openssh.com AAAAHHNzaC1kc3MtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgsKvMxIv4viCNQX7z8K4/R5jronpZGf' + + b'ydpoBoh2Cx5dgAAACBAPV/b5FknU8e56TWAGLRQ0v3c3f5jAS0txcwqtYLHLulTqyMcLL0MyzWxXv77MpjTMwEjWXLbfNWdk/qmsjfBynzs' + + b'2nSZ7clVsqt/ZOadcBFEhqZM0l+1ZCPkhQiqsD2aodGbkVcJgqL5Z5krzB5MTey7c8rlAAxKOjfs70Bg8MPAAAAFQCW466dSEu2Pf0u8AA5' + + b'SHgH0i/xuwAAAIBc23gfmvGC+oaUAXiak17kH6NvOSJXZBdk/8CyGK6yL+CHKrKyffe6BbiVXwC6sUIa9j4YsFeyYwPFGBtfLuNUmgyKYTJ' + + b'cCM2zJLBykmTIvjSdRaYGNRkyi8GnzVV2lWxQ+4m4UGeTPbPN/OG4B0NwDbBJGbVJv0xJPq2EBKoUdgAAAIAyrFxGDLtOZFZ2fgONVaKaap' + + b'EpJ5f3qPhLDXxVQ/BKVUkURA4AHHyXF2AMiiOOiHLrO5xsEGUyW+OISFm+6m17cEPNixA7G1fBniLvyVv2woyYW3kaY4J9z266kAFzFWVNg' + + b'wr+T7MY0hEvct8VFA97JMRQ7c8c/tNDaL7uqV46QQAAAAAAAAAAAAAAAQAAAAR0ZXN0AAAAAAAAAAAAAAAA//////////8AAAAAAAAAAAAA' + + b'AAAAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOf55Wc0yzaJPtxXxBGZKmAUozbYXwxZGFS1c/FaJbwLpq/' + + b'wvanQKM01uU73swNIt+ZFra9kRSi21xjzgMPn7U0AAABkAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAABJAAAAIGmlKa/riG7+EpoW6dTJY6' + + b'0N8BrEcniKgOxdRM1EPJ2DAAAAIQDnK4stvbvS+Bn0/42Was7uOfJtnLYXs5EuB2L3uejPcQ== ansible@ansible-host' +) +DSA_FINGERPRINT = 'SHA256:YCdJ2lYU+FSkWUud7zg1SJszprXoRGNU/GVcqXUjgC8' +# Type: ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate +# Public key: ECDSA-CERT SHA256:w9lp4zGRJShhm4DzO3ulVm0BEcR0PMjrM6VanQo4C0w +# Signing CA: ED25519 SHA256:NP4JdfkCopbjwMepq0aPrpMz13cNmEd+uDOxC/j9N40 +# Key ID: "test" +# Serial: 0 +# Valid: forever +# Principals: (none) +# Critical Options: +# force-command /usr/bin/csh +# Extensions: +# permit-X11-forwarding +# permit-agent-forwarding +# permit-port-forwarding +# permit-pty +# permit-user-rc +ECDSA_CERT_SIGNED_BY_ED25519_VALID_OPTS = ( + b'ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgtC' + + b'ips7/sOOOTAgiawGlQhM6pb26t0FfQ1jG60m+tOg0AAAAIbmlzdHAyNTYAAABBBOf55Wc0yzaJPtxXxBGZKmAUozbYXwxZGFS1c/FaJbwLp' + + b'q/wvanQKM01uU73swNIt+ZFra9kRSi21xjzgMPn7U0AAAAAAAAAAAAAAAEAAAAEdGVzdAAAAAAAAAAAAAAAAP//////////AAAAJQAAAA1m' + + b'b3JjZS1jb21tYW5kAAAAEAAAAAwvdXNyL2Jpbi9jc2gAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW5' + + b'0LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLX' + + b'JjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAII3qYBforim0x87UXpaTDNFnhFTyb+TPCJVQpEAOHTL6AAAAUwAAAAtzc2gtZWQyN' + + b'TUxOQAAAEAdp3eOLRN5t2wW29TBWbz604uuXg88jH4RA4HDhbRupa/x2rN3j6iZQ4VXPLA4JtdfIslHFkH6HUlxU8XsoJwP ' + + b'ansible@ansible-host' +) +ECDSA_FINGERPRINT = 'SHA256:w9lp4zGRJShhm4DzO3ulVm0BEcR0PMjrM6VanQo4C0w' +# Type: ssh-ed25519-cert-v01@openssh.com user certificate +# Public key: ED25519-CERT SHA256:NP4JdfkCopbjwMepq0aPrpMz13cNmEd+uDOxC/j9N40 +# Signing CA: RSA SHA256:SvUwwUer4AwsdePYseJR3LcZS8lnKi6BqiL51Dop030 +# Key ID: "test" +# Serial: 0 +# Valid: forever +# Principals: (none) +# Critical Options: +# test UNKNOWN OPTION (len 13) +# Extensions: +# test UNKNOWN OPTION (len 0) +ED25519_CERT_SIGNED_BY_RSA_INVALID_OPTS = ( + b'ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIP034YpKn6BDcwxqFnVrKt' + + b'kNX7k6X7hxZ7lADp5LAxHrAAAAII3qYBforim0x87UXpaTDNFnhFTyb+TPCJVQpEAOHTL6AAAAAAAAAAAAAAABAAAABHRlc3QAAAAAAAAAA' + + b'AAAAAD//////////wAAABkAAAAEdGVzdAAAAA0AAAAJdW5kZWZpbmVkAAAADAAAAAR0ZXN0AAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAAD' + + b'AQABAAABAQDKYIJtpFaWpTNNifmuV3DM9BBdngMG28jWPy4C/SoZg4EP7mkYUsG6hN+LgjOL17YEF7bKDEWPl9sQS92iD+AuAPrjnHVQ9VG' + + b'5hbTYiQAaicj6hxqBoNqGQWxDzhZL4B35MgqmoUOBGnzYA/fKgqhRVzOXbWFxKLtzSJzB+Z+kmeoBzq+4MazL4BkoyPZMrIMnvxiluv+kqE' + + b'9SWeJ/5e7WXdtbYTnSR4WN3gW/BMKEoKQk/UGwuPvCiRq+y8LorJP4B1Wfwlm/meqtbTidXyCcQPR9xWpce3rRjLLT6cimUjWrbx7Q1Slsy' + + b'pdkclgPSTu9Jg457am8tnQUgnL7VdetAAABDwAAAAdzc2gtcnNhAAABAMZLNacwOMNexYUaFK1nU0JPQTv4fM73QDG3xURtDsIbI6DAcA1y' + + b'KkvgjJcxlZHx0APJ+i1lWNAvPeOmuPTioymjIEuwxi0VGuAoVKgjmIy6aXH2z3YMxy9cGOq6LNfI4c58iBHR5ejVHAzvIg3rowypVsCGugL' + + b'7WJpz3eypBJt4TglwRTJpp54IMN2CyDQm0N97x9ris8jQQHlCF2EgZp1u4aOiZJTSJ5d4hapO0uZwXOI9AIWy/lmx0/6jX07MWrs4iXpfiF' + + b'5T4s6kEn7YW4SaJ0Z7xGp3V0vDOxh+jwHZGD5GM449Il6QxQwDY5BSJq+iMR467yaIjw2g8Kt4ZiU= ansible@ansible-host' +) +ED25519_FINGERPRINT = 'SHA256:NP4JdfkCopbjwMepq0aPrpMz13cNmEd+uDOxC/j9N40' +# garbage +INVALID_DATA = b'yDspTN+BJzvIK2Q+CRD3qBDVSi+YqSxwyz432VEaHKlXbuLURirY0QpuBCqgR6tCtWW5vEGkXKZ3' + +VALID_OPTS = [OpensshCertificateOption('critical', 'force-command', '/usr/bin/csh')] +INVALID_OPTS = [OpensshCertificateOption('critical', 'test', 'undefined')] +VALID_EXTENSIONS = [ + OpensshCertificateOption('extension', 'permit-x11-forwarding', ''), + OpensshCertificateOption('extension', 'permit-agent-forwarding', ''), + OpensshCertificateOption('extension', 'permit-port-forwarding', ''), + OpensshCertificateOption('extension', 'permit-pty', ''), + OpensshCertificateOption('extension', 'permit-user-rc', ''), +] +INVALID_EXTENSIONS = [OpensshCertificateOption('extension', 'test', '')] + +VALID_TIME_PARAMETERS = [ + (0, "always", "always", 0, + 0xFFFFFFFFFFFFFFFF, "forever", "forever", 253402300800, + ""), + ("always", "always", "always", 0, + "forever", "forever", "forever", 253402300800, + ""), + (315532800, "1980-01-01T00:00:00", "19800101000000", 315532800, + 631152000, "1990-01-01T00:00:00", "19900101000000", 631152000, + "19800101000000:19900101000000"), + ("1980-01-01", "1980-01-01T00:00:00", "19800101000000", 315532800, + "1990-01-01", "1990-01-01T00:00:00", "19900101000000", 631152000, + "19800101000000:19900101000000"), + ("1980-01-01 00:00:00", "1980-01-01T00:00:00", "19800101000000", 315532800, + "1990-01-01 00:00:00", "1990-01-01T00:00:00", "19900101000000", 631152000, + "19800101000000:19900101000000"), + ("1980-01-01T00:00:00", "1980-01-01T00:00:00", "19800101000000", 315532800, + "1990-01-01T00:00:00", "1990-01-01T00:00:00", "19900101000000", 631152000, + "19800101000000:19900101000000"), + ("always", "always", "always", 0, + "1990-01-01T00:00:00", "1990-01-01T00:00:00", "19900101000000", 631152000, + "always:19900101000000"), + ("1980-01-01", "1980-01-01T00:00:00", "19800101000000", 315532800, + "forever", "forever", "forever", 253402300800, + "19800101000000:forever"), +] + +INVALID_TIME_PARAMETERS = [ + (-1, 0xFFFFFFFFFFFFFFFFFF), + ("never", "ever"), + ("01-01-1980", "01-01-1990"), + (1, 0), +] + +VALID_VALIDITY_TEST = [ + ("always", "forever", "2000-01-01"), + ("1999-12-31", "2000-01-02", "2000-01-01"), + ("1999-12-31 23:59:00", "2000-01-01 00:01:00", "2000-01-01 00:00:00"), + ("1999-12-31 23:59:59", "2000-01-01 00:00:01", "2000-01-01 00:00:00"), +] + +INVALID_VALIDITY_TEST = [ + ("always", "forever", "1969-12-31"), + ("always", "2000-01-01", "2000-01-02"), + ("2000-01-01", "forever", "1999-12-31"), + ("2000-01-01 00:00:00", "2000-01-01 00:00:01", "2000-01-01 00:00:02"), +] + +VALID_OPTIONS = [ + ("force-command=/usr/bin/csh", OpensshCertificateOption('critical', 'force-command', '/usr/bin/csh')), + ("Force-Command=/Usr/Bin/Csh", OpensshCertificateOption('critical', 'force-command', '/Usr/Bin/Csh')), + ("permit-x11-forwarding", OpensshCertificateOption('extension', 'permit-x11-forwarding', '')), + ("permit-X11-forwarding", OpensshCertificateOption('extension', 'permit-x11-forwarding', '')), + ("critical:foo=bar", OpensshCertificateOption('critical', 'foo', 'bar')), + ("extension:foo", OpensshCertificateOption('extension', 'foo', '')), +] + +INVALID_OPTIONS = [ + "foobar", + "foo=bar", + 'foo:bar=baz', + [], +] + + +def test_rsa_certificate(tmpdir): + cert_file = tmpdir / 'id_rsa-cert.pub' + cert_file.write(RSA_CERT_SIGNED_BY_DSA, mode='wb') + + cert = OpensshCertificate.load(str(cert_file)) + assert cert.key_id == 'test' + assert cert.serial == 0 + assert cert.type_string == 'ssh-rsa-cert-v01@openssh.com' + assert cert.public_key == RSA_FINGERPRINT + assert cert.signing_key == DSA_FINGERPRINT + + +def test_dsa_certificate(tmpdir): + cert_file = tmpdir / 'id_dsa-cert.pub' + cert_file.write(DSA_CERT_SIGNED_BY_ECDSA_NO_OPTS) + + cert = OpensshCertificate.load(str(cert_file)) + + assert cert.type_string == 'ssh-dss-cert-v01@openssh.com' + assert cert.public_key == DSA_FINGERPRINT + assert cert.signing_key == ECDSA_FINGERPRINT + assert cert.critical_options == [] + assert cert.extensions == [] + + +def test_ecdsa_certificate(tmpdir): + cert_file = tmpdir / 'id_ecdsa-cert.pub' + cert_file.write(ECDSA_CERT_SIGNED_BY_ED25519_VALID_OPTS) + + cert = OpensshCertificate.load(str(cert_file)) + assert cert.type_string == 'ecdsa-sha2-nistp256-cert-v01@openssh.com' + assert cert.public_key == ECDSA_FINGERPRINT + assert cert.signing_key == ED25519_FINGERPRINT + assert cert.critical_options == VALID_OPTS + assert cert.extensions == VALID_EXTENSIONS + + +def test_ed25519_certificate(tmpdir): + cert_file = tmpdir / 'id_ed25519-cert.pub' + cert_file.write(ED25519_CERT_SIGNED_BY_RSA_INVALID_OPTS) + + cert = OpensshCertificate.load(str(cert_file)) + assert cert.type_string == 'ssh-ed25519-cert-v01@openssh.com' + assert cert.public_key == ED25519_FINGERPRINT + assert cert.signing_key == RSA_FINGERPRINT + assert cert.critical_options == INVALID_OPTS + assert cert.extensions == INVALID_EXTENSIONS + + +def test_invalid_data(tmpdir): + result = False + cert_file = tmpdir / 'invalid-cert.pub' + cert_file.write(INVALID_DATA) + + try: + OpensshCertificate.load(str(cert_file)) + except ValueError: + result = True + assert result + + +@pytest.mark.parametrize( + "valid_from,valid_from_hr,valid_from_openssh,valid_from_timestamp," + + "valid_to,valid_to_hr,valid_to_openssh,valid_to_timestamp," + + "validity_string", + VALID_TIME_PARAMETERS +) +def test_valid_time_parameters(valid_from, valid_from_hr, valid_from_openssh, valid_from_timestamp, + valid_to, valid_to_hr, valid_to_openssh, valid_to_timestamp, + validity_string): + time_parameters = OpensshCertificateTimeParameters( + valid_from=valid_from, + valid_to=valid_to + ) + assert time_parameters.valid_from(date_format="human_readable") == valid_from_hr + assert time_parameters.valid_from(date_format="openssh") == valid_from_openssh + assert time_parameters.valid_from(date_format="timestamp") == valid_from_timestamp + assert time_parameters.valid_to(date_format="human_readable") == valid_to_hr + assert time_parameters.valid_to(date_format="openssh") == valid_to_openssh + assert time_parameters.valid_to(date_format="timestamp") == valid_to_timestamp + assert time_parameters.validity_string == validity_string + + +@pytest.mark.parametrize("valid_from,valid_to", INVALID_TIME_PARAMETERS) +def test_invalid_time_parameters(valid_from, valid_to): + with pytest.raises(ValueError): + OpensshCertificateTimeParameters(valid_from, valid_to) + + +@pytest.mark.parametrize("valid_from,valid_to,valid_at", VALID_VALIDITY_TEST) +def test_valid_validity_test(valid_from, valid_to, valid_at): + assert OpensshCertificateTimeParameters(valid_from, valid_to).within_range(valid_at) + + +@pytest.mark.parametrize("valid_from,valid_to,valid_at", INVALID_VALIDITY_TEST) +def test_invalid_validity_test(valid_from, valid_to, valid_at): + assert not OpensshCertificateTimeParameters(valid_from, valid_to).within_range(valid_at) + + +@pytest.mark.parametrize("option_string,option_object", VALID_OPTIONS) +def test_valid_options(option_string, option_object): + assert OpensshCertificateOption.from_string(option_string) == option_object + + +@pytest.mark.parametrize("option_string", INVALID_OPTIONS) +def test_invalid_options(option_string): + with pytest.raises(ValueError): + OpensshCertificateOption.from_string(option_string) + + +def test_parse_option_list(): + critical_options, extensions = parse_option_list(['force-command=/usr/bin/csh']) + + critical_option_objects = [ + OpensshCertificateOption.from_string('force-command=/usr/bin/csh'), + ] + + extension_objects = [ + OpensshCertificateOption.from_string('permit-x11-forwarding'), + OpensshCertificateOption.from_string('permit-agent-forwarding'), + OpensshCertificateOption.from_string('permit-port-forwarding'), + OpensshCertificateOption.from_string('permit-user-rc'), + OpensshCertificateOption.from_string('permit-pty'), + ] + + assert set(critical_options) == set(critical_option_objects) + assert set(extensions) == set(extension_objects) + + +def test_parse_option_list_with_directives(): + critical_options, extensions = parse_option_list(['clear', 'no-pty', 'permit-pty', 'permit-user-rc']) + + extension_objects = [ + OpensshCertificateOption.from_string('permit-user-rc'), + OpensshCertificateOption.from_string('permit-pty'), + ] + + assert set(critical_options) == set() + assert set(extensions) == set(extension_objects) + + +def test_parse_option_list_case_sensitivity(): + critical_options, extensions = parse_option_list(['CLEAR', 'no-X11-forwarding', 'permit-X11-forwarding']) + + extension_objects = [ + OpensshCertificateOption.from_string('permit-x11-forwarding'), + ] + + assert set(critical_options) == set() + assert set(extensions) == set(extension_objects) diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/openssh/test_cryptography.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/openssh/test_cryptography.py new file mode 100644 index 000000000..b4d52bd0d --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/openssh/test_cryptography.py @@ -0,0 +1,401 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +import os.path +from getpass import getuser +from os import remove, rmdir +from socket import gethostname +from tempfile import mkdtemp + +from ansible_collections.community.crypto.plugins.module_utils.openssh.cryptography import ( + HAS_OPENSSH_SUPPORT, + InvalidCommentError, + InvalidPrivateKeyFileError, + InvalidPublicKeyFileError, + InvalidKeySizeError, + InvalidKeyTypeError, + InvalidPassphraseError, + OpensshKeypair +) + +DEFAULT_KEY_PARAMS = [ + ( + 'rsa', + None, + None, + None, + ), + ( + 'dsa', + None, + None, + None, + ), + ( + 'ecdsa', + None, + None, + None, + ), + ( + 'ed25519', + None, + None, + None, + ), +] + +VALID_USER_KEY_PARAMS = [ + ( + 'rsa', + 8192, + 'change_me'.encode('UTF-8'), + 'comment', + ), + ( + 'dsa', + 1024, + 'change_me'.encode('UTF-8'), + 'comment', + ), + ( + 'ecdsa', + 521, + 'change_me'.encode('UTF-8'), + 'comment', + ), + ( + 'ed25519', + 256, + 'change_me'.encode('UTF-8'), + 'comment', + ), +] + +INVALID_USER_KEY_PARAMS = [ + ( + 'dne', + None, + None, + None, + ), + ( + 'rsa', + None, + [1, 2, 3], + 'comment', + ), + ( + 'ecdsa', + None, + None, + [1, 2, 3], + ), +] + +INVALID_KEY_SIZES = [ + ( + 'rsa', + 1023, + None, + None, + ), + ( + 'rsa', + 16385, + None, + None, + ), + ( + 'dsa', + 256, + None, + None, + ), + ( + 'ecdsa', + 1024, + None, + None, + ), + ( + 'ed25519', + 1024, + None, + None, + ), +] + + +@pytest.mark.parametrize("keytype,size,passphrase,comment", DEFAULT_KEY_PARAMS) +@pytest.mark.skipif(not HAS_OPENSSH_SUPPORT, reason="requires cryptography") +def test_default_key_params(keytype, size, passphrase, comment): + result = True + + default_sizes = { + 'rsa': 2048, + 'dsa': 1024, + 'ecdsa': 256, + 'ed25519': 256, + } + + default_comment = "%s@%s" % (getuser(), gethostname()) + pair = OpensshKeypair.generate(keytype=keytype, size=size, passphrase=passphrase, comment=comment) + try: + pair = OpensshKeypair.generate(keytype=keytype, size=size, passphrase=passphrase, comment=comment) + if pair.size != default_sizes[pair.key_type] or pair.comment != default_comment: + result = False + except Exception as e: + print(e) + result = False + + assert result + + +@pytest.mark.parametrize("keytype,size,passphrase,comment", VALID_USER_KEY_PARAMS) +@pytest.mark.skipif(not HAS_OPENSSH_SUPPORT, reason="requires cryptography") +def test_valid_user_key_params(keytype, size, passphrase, comment): + result = True + + try: + pair = OpensshKeypair.generate(keytype=keytype, size=size, passphrase=passphrase, comment=comment) + if pair.key_type != keytype or pair.size != size or pair.comment != comment: + result = False + except Exception as e: + print(e) + result = False + + assert result + + +@pytest.mark.parametrize("keytype,size,passphrase,comment", INVALID_USER_KEY_PARAMS) +@pytest.mark.skipif(not HAS_OPENSSH_SUPPORT, reason="requires cryptography") +def test_invalid_user_key_params(keytype, size, passphrase, comment): + result = False + + try: + OpensshKeypair.generate(keytype=keytype, size=size, passphrase=passphrase, comment=comment) + except (InvalidCommentError, InvalidKeyTypeError, InvalidPassphraseError): + result = True + except Exception as e: + print(e) + pass + + assert result + + +@pytest.mark.parametrize("keytype,size,passphrase,comment", INVALID_KEY_SIZES) +@pytest.mark.skipif(not HAS_OPENSSH_SUPPORT, reason="requires cryptography") +def test_invalid_key_sizes(keytype, size, passphrase, comment): + result = False + + try: + OpensshKeypair.generate(keytype=keytype, size=size, passphrase=passphrase, comment=comment) + except InvalidKeySizeError: + result = True + except Exception as e: + print(e) + pass + + assert result + + +@pytest.mark.skipif(not HAS_OPENSSH_SUPPORT, reason="requires cryptography") +def test_valid_comment_update(): + + pair = OpensshKeypair.generate() + new_comment = "comment" + try: + pair.comment = new_comment + except Exception as e: + print(e) + pass + + assert pair.comment == new_comment and pair.public_key.split(b' ', 2)[2].decode() == new_comment + + +@pytest.mark.skipif(not HAS_OPENSSH_SUPPORT, reason="requires cryptography") +def test_invalid_comment_update(): + result = False + + pair = OpensshKeypair.generate() + new_comment = [1, 2, 3] + try: + pair.comment = new_comment + except InvalidCommentError: + result = True + + assert result + + +@pytest.mark.skipif(not HAS_OPENSSH_SUPPORT, reason="requires cryptography") +def test_valid_passphrase_update(): + result = False + + passphrase = "change_me".encode('UTF-8') + + try: + tmpdir = mkdtemp() + keyfilename = os.path.join(tmpdir, "id_rsa") + + pair1 = OpensshKeypair.generate() + pair1.update_passphrase(passphrase) + + with open(keyfilename, "w+b") as keyfile: + keyfile.write(pair1.private_key) + + with open(keyfilename + '.pub', "w+b") as pubkeyfile: + pubkeyfile.write(pair1.public_key) + + pair2 = OpensshKeypair.load(path=keyfilename, passphrase=passphrase) + + if pair1 == pair2: + result = True + finally: + if os.path.exists(keyfilename): + remove(keyfilename) + if os.path.exists(keyfilename + '.pub'): + remove(keyfilename + '.pub') + if os.path.exists(tmpdir): + rmdir(tmpdir) + + assert result + + +@pytest.mark.skipif(not HAS_OPENSSH_SUPPORT, reason="requires cryptography") +def test_invalid_passphrase_update(): + result = False + + passphrase = [1, 2, 3] + pair = OpensshKeypair.generate() + try: + pair.update_passphrase(passphrase) + except InvalidPassphraseError: + result = True + + assert result + + +@pytest.mark.skipif(not HAS_OPENSSH_SUPPORT, reason="requires cryptography") +def test_invalid_privatekey(): + result = False + + try: + tmpdir = mkdtemp() + keyfilename = os.path.join(tmpdir, "id_rsa") + + pair = OpensshKeypair.generate() + + with open(keyfilename, "w+b") as keyfile: + keyfile.write(pair.private_key[1:]) + + with open(keyfilename + '.pub', "w+b") as pubkeyfile: + pubkeyfile.write(pair.public_key) + + OpensshKeypair.load(path=keyfilename) + except InvalidPrivateKeyFileError: + result = True + finally: + if os.path.exists(keyfilename): + remove(keyfilename) + if os.path.exists(keyfilename + '.pub'): + remove(keyfilename + '.pub') + if os.path.exists(tmpdir): + rmdir(tmpdir) + + assert result + + +@pytest.mark.skipif(not HAS_OPENSSH_SUPPORT, reason="requires cryptography") +def test_mismatched_keypair(): + result = False + + try: + tmpdir = mkdtemp() + keyfilename = os.path.join(tmpdir, "id_rsa") + + pair1 = OpensshKeypair.generate() + pair2 = OpensshKeypair.generate() + + with open(keyfilename, "w+b") as keyfile: + keyfile.write(pair1.private_key) + + with open(keyfilename + '.pub', "w+b") as pubkeyfile: + pubkeyfile.write(pair2.public_key) + + OpensshKeypair.load(path=keyfilename) + except InvalidPublicKeyFileError: + result = True + finally: + if os.path.exists(keyfilename): + remove(keyfilename) + if os.path.exists(keyfilename + '.pub'): + remove(keyfilename + '.pub') + if os.path.exists(tmpdir): + rmdir(tmpdir) + + assert result + + +@pytest.mark.skipif(not HAS_OPENSSH_SUPPORT, reason="requires cryptography") +def test_keypair_comparison(): + assert OpensshKeypair.generate() != OpensshKeypair.generate() + assert OpensshKeypair.generate() != OpensshKeypair.generate(keytype='dsa') + assert OpensshKeypair.generate() != OpensshKeypair.generate(keytype='ed25519') + assert OpensshKeypair.generate(keytype='ed25519') != OpensshKeypair.generate(keytype='ed25519') + try: + tmpdir = mkdtemp() + + keys = { + 'rsa': { + 'pair': OpensshKeypair.generate(), + 'filename': os.path.join(tmpdir, "id_rsa"), + }, + 'dsa': { + 'pair': OpensshKeypair.generate(keytype='dsa', passphrase='change_me'.encode('UTF-8')), + 'filename': os.path.join(tmpdir, "id_dsa"), + }, + 'ed25519': { + 'pair': OpensshKeypair.generate(keytype='ed25519'), + 'filename': os.path.join(tmpdir, "id_ed25519"), + } + } + + for v in keys.values(): + with open(v['filename'], "w+b") as keyfile: + keyfile.write(v['pair'].private_key) + with open(v['filename'] + '.pub', "w+b") as pubkeyfile: + pubkeyfile.write(v['pair'].public_key) + + assert keys['rsa']['pair'] == OpensshKeypair.load(path=keys['rsa']['filename']) + + loaded_dsa_key = OpensshKeypair.load(path=keys['dsa']['filename'], passphrase='change_me'.encode('UTF-8')) + assert keys['dsa']['pair'] == loaded_dsa_key + + loaded_dsa_key.update_passphrase('change_me_again'.encode('UTF-8')) + assert keys['dsa']['pair'] != loaded_dsa_key + + loaded_dsa_key.update_passphrase('change_me'.encode('UTF-8')) + assert keys['dsa']['pair'] == loaded_dsa_key + + loaded_dsa_key.comment = "comment" + assert keys['dsa']['pair'] != loaded_dsa_key + + assert keys['ed25519']['pair'] == OpensshKeypair.load(path=keys['ed25519']['filename']) + finally: + for v in keys.values(): + if os.path.exists(v['filename']): + remove(v['filename']) + if os.path.exists(v['filename'] + '.pub'): + remove(v['filename'] + '.pub') + if os.path.exists(tmpdir): + rmdir(tmpdir) + assert OpensshKeypair.generate() != [] diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/openssh/test_utils.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/openssh/test_utils.py new file mode 100644 index 000000000..2ea537d2b --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/openssh/test_utils.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import ( + parse_openssh_version, + OpensshParser, + _OpensshWriter +) + +SSH_VERSION_STRING = "OpenSSH_7.9p1, OpenSSL 1.1.0i-fips 14 Aug 2018" +SSH_VERSION_NUMBER = "7.9" + +VALID_BOOLEAN = [ + True, + False +] +INVALID_BOOLEAN = [ + 0x02 +] +VALID_UINT32 = [ + 0x00, + 0x01, + 0x01234567, + 0xFFFFFFFF, +] +INVALID_UINT32 = [ + 0xFFFFFFFFF, + -1, +] +VALID_UINT64 = [ + 0x00, + 0x01, + 0x0123456789ABCDEF, + 0xFFFFFFFFFFFFFFFF, +] +INVALID_UINT64 = [ + 0xFFFFFFFFFFFFFFFFF, + -1, +] +VALID_STRING = [ + b'test string', +] +INVALID_STRING = [ + [], +] +# See https://datatracker.ietf.org/doc/html/rfc4251#section-5 for examples source +VALID_MPINT = [ + 0x00, + 0x9a378f9b2e332a7, + 0x80, + -0x1234, + -0xdeadbeef, + # Additional large int test + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, +] +INVALID_MPINT = [ + [], +] + + +def test_parse_openssh_version(): + assert parse_openssh_version(SSH_VERSION_STRING) == SSH_VERSION_NUMBER + + +@pytest.mark.parametrize("boolean", VALID_BOOLEAN) +def test_valid_boolean(boolean): + assert OpensshParser(_OpensshWriter().boolean(boolean).bytes()).boolean() == boolean + + +@pytest.mark.parametrize("boolean", INVALID_BOOLEAN) +def test_invalid_boolean(boolean): + with pytest.raises(TypeError): + _OpensshWriter().boolean(boolean) + + +@pytest.mark.parametrize("uint32", VALID_UINT32) +def test_valid_uint32(uint32): + assert OpensshParser(_OpensshWriter().uint32(uint32).bytes()).uint32() == uint32 + + +@pytest.mark.parametrize("uint32", INVALID_UINT32) +def test_invalid_uint32(uint32): + with pytest.raises(ValueError): + _OpensshWriter().uint32(uint32) + + +@pytest.mark.parametrize("uint64", VALID_UINT64) +def test_valid_uint64(uint64): + assert OpensshParser(_OpensshWriter().uint64(uint64).bytes()).uint64() == uint64 + + +@pytest.mark.parametrize("uint64", INVALID_UINT64) +def test_invalid_uint64(uint64): + with pytest.raises(ValueError): + _OpensshWriter().uint64(uint64) + + +@pytest.mark.parametrize("ssh_string", VALID_STRING) +def test_valid_string(ssh_string): + assert OpensshParser(_OpensshWriter().string(ssh_string).bytes()).string() == ssh_string + + +@pytest.mark.parametrize("ssh_string", INVALID_STRING) +def test_invalid_string(ssh_string): + with pytest.raises(TypeError): + _OpensshWriter().string(ssh_string) + + +@pytest.mark.parametrize("mpint", VALID_MPINT) +def test_valid_mpint(mpint): + assert OpensshParser(_OpensshWriter().mpint(mpint).bytes()).mpint() == mpint + + +@pytest.mark.parametrize("mpint", INVALID_MPINT) +def test_invalid_mpint(mpint): + with pytest.raises(TypeError): + _OpensshWriter().mpint(mpint) + + +def test_valid_seek(): + buffer = bytearray(b'buffer') + parser = OpensshParser(buffer) + parser.seek(len(buffer)) + assert parser.remaining_bytes() == 0 + parser.seek(-len(buffer)) + assert parser.remaining_bytes() == len(buffer) + + +def test_invalid_seek(): + result = False + buffer = b'buffer' + parser = OpensshParser(buffer) + + with pytest.raises(ValueError): + parser.seek(len(buffer) + 1) + + with pytest.raises(ValueError): + parser.seek(-1) + + +def test_writer_bytes(): + buffer = bytearray(b'buffer') + assert _OpensshWriter(buffer).bytes() == buffer diff --git a/ansible_collections/community/crypto/tests/unit/plugins/modules/test_luks_device.py b/ansible_collections/community/crypto/tests/unit/plugins/modules/test_luks_device.py new file mode 100644 index 000000000..c773640c6 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/modules/test_luks_device.py @@ -0,0 +1,320 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest +from ansible_collections.community.crypto.plugins.modules import luks_device + + +class DummyModule(object): + # module to mock AnsibleModule class + def __init__(self): + self.params = dict() + + def fail_json(self, msg=""): + raise ValueError(msg) + + def get_bin_path(self, command, dummy): + return command + + +# ===== Handler & CryptHandler methods tests ===== + +def test_generate_luks_name(monkeypatch): + module = DummyModule() + monkeypatch.setattr(luks_device.Handler, "_run_command", + lambda x, y: [0, "UUID", ""]) + crypt = luks_device.CryptHandler(module) + assert crypt.generate_luks_name("/dev/dummy") == "luks-UUID" + + +def test_get_container_name_by_device(monkeypatch): + module = DummyModule() + monkeypatch.setattr(luks_device.Handler, "_run_command", + lambda x, y: [0, "crypt container_name", ""]) + crypt = luks_device.CryptHandler(module) + assert crypt.get_container_name_by_device("/dev/dummy") == "container_name" + + +def test_get_container_device_by_name(monkeypatch): + module = DummyModule() + monkeypatch.setattr(luks_device.Handler, "_run_command", + lambda x, y: [0, "device: /dev/luksdevice", ""]) + crypt = luks_device.CryptHandler(module) + assert crypt.get_container_device_by_name("dummy") == "/dev/luksdevice" + + +def test_run_luks_remove(monkeypatch): + def run_command_check(self, command): + # check that wipefs command is actually called + assert command[0] == "wipefs" + return [0, "", ""] + + module = DummyModule() + monkeypatch.setattr(luks_device.CryptHandler, + "get_container_name_by_device", + lambda x, y: None) + monkeypatch.setattr(luks_device.Handler, + "_run_command", + run_command_check) + monkeypatch.setattr(luks_device, + "wipe_luks_headers", + lambda device: True) + crypt = luks_device.CryptHandler(module) + crypt.run_luks_remove("dummy") + + +# ===== ConditionsHandler methods data and tests ===== + +# device, key, passphrase, state, is_luks, label, cipher, hash, expected +LUKS_CREATE_DATA = ( + ("dummy", "key", None, "present", False, None, "dummy", "dummy", True), + (None, "key", None, "present", False, None, "dummy", "dummy", False), + (None, "key", None, "present", False, "labelName", "dummy", "dummy", True), + ("dummy", None, None, "present", False, None, "dummy", "dummy", False), + ("dummy", "key", None, "absent", False, None, "dummy", "dummy", False), + ("dummy", "key", None, "opened", True, None, "dummy", "dummy", False), + ("dummy", "key", None, "closed", True, None, "dummy", "dummy", False), + ("dummy", "key", None, "present", True, None, "dummy", "dummy", False), + ("dummy", None, "foo", "present", False, None, "dummy", "dummy", True), + (None, None, "bar", "present", False, None, "dummy", "dummy", False), + (None, None, "baz", "present", False, "labelName", "dummy", "dummy", True), + ("dummy", None, None, "present", False, None, "dummy", "dummy", False), + ("dummy", None, "quz", "absent", False, None, "dummy", "dummy", False), + ("dummy", None, "qux", "opened", True, None, "dummy", "dummy", False), + ("dummy", None, "quux", "closed", True, None, "dummy", "dummy", False), + ("dummy", None, "corge", "present", True, None, "dummy", "dummy", False), + ("dummy", "key", None, "present", False, None, None, None, True), + ("dummy", "key", None, "present", False, None, None, "dummy", True), + ("dummy", "key", None, "present", False, None, "dummy", None, True)) + +# device, state, is_luks, expected +LUKS_REMOVE_DATA = ( + ("dummy", "absent", True, True), + (None, "absent", True, False), + ("dummy", "present", True, False), + ("dummy", "absent", False, False)) + +# device, key, passphrase, state, name, name_by_dev, expected +LUKS_OPEN_DATA = ( + ("dummy", "key", None, "present", "name", None, False), + ("dummy", "key", None, "absent", "name", None, False), + ("dummy", "key", None, "closed", "name", None, False), + ("dummy", "key", None, "opened", "name", None, True), + (None, "key", None, "opened", "name", None, False), + ("dummy", None, None, "opened", "name", None, False), + ("dummy", "key", None, "opened", "name", "name", False), + ("dummy", "key", None, "opened", "beer", "name", "exception"), + ("dummy", None, "foo", "present", "name", None, False), + ("dummy", None, "bar", "absent", "name", None, False), + ("dummy", None, "baz", "closed", "name", None, False), + ("dummy", None, "qux", "opened", "name", None, True), + (None, None, "quux", "opened", "name", None, False), + ("dummy", None, None, "opened", "name", None, False), + ("dummy", None, "quuz", "opened", "name", "name", False), + ("dummy", None, "corge", "opened", "beer", "name", "exception")) + +# device, dev_by_name, name, name_by_dev, state, label, expected +LUKS_CLOSE_DATA = ( + ("dummy", "dummy", "name", "name", "present", None, False), + ("dummy", "dummy", "name", "name", "absent", None, False), + ("dummy", "dummy", "name", "name", "opened", None, False), + ("dummy", "dummy", "name", "name", "closed", None, True), + (None, "dummy", "name", "name", "closed", None, True), + ("dummy", "dummy", None, "name", "closed", None, True), + (None, "dummy", None, "name", "closed", None, False)) + +# device, key, passphrase, new_key, new_passphrase, state, label, expected +LUKS_ADD_KEY_DATA = ( + ("dummy", "key", None, "new_key", None, "present", None, True), + (None, "key", None, "new_key", None, "present", "labelName", True), + (None, "key", None, "new_key", None, "present", None, False), + ("dummy", None, None, "new_key", None, "present", None, False), + ("dummy", "key", None, None, None, "present", None, False), + ("dummy", "key", None, "new_key", None, "absent", None, "exception"), + ("dummy", None, "pass", "new_key", None, "present", None, True), + (None, None, "pass", "new_key", None, "present", "labelName", True), + ("dummy", "key", None, None, "new_pass", "present", None, True), + (None, "key", None, None, "new_pass", "present", "labelName", True), + (None, "key", None, None, "new_pass", "present", None, False), + ("dummy", None, None, None, "new_pass", "present", None, False), + ("dummy", "key", None, None, None, "present", None, False), + ("dummy", "key", None, None, "new_pass", "absent", None, "exception"), + ("dummy", None, "pass", None, "new_pass", "present", None, True), + (None, None, "pass", None, "new_pass", "present", "labelName", True)) + +# device, remove_key, remove_passphrase, state, label, expected +LUKS_REMOVE_KEY_DATA = ( + ("dummy", "key", None, "present", None, True), + (None, "key", None, "present", None, False), + (None, "key", None, "present", "labelName", True), + ("dummy", None, None, "present", None, False), + ("dummy", "key", None, "absent", None, "exception"), + ("dummy", None, "foo", "present", None, True), + (None, None, "foo", "present", None, False), + (None, None, "foo", "present", "labelName", True), + ("dummy", None, None, "present", None, False), + ("dummy", None, "foo", "absent", None, "exception")) + + +@pytest.mark.parametrize("device, keyfile, passphrase, state, is_luks, " + + "label, cipher, hash_, expected", + ((d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8]) + for d in LUKS_CREATE_DATA)) +def test_luks_create(device, keyfile, passphrase, state, is_luks, label, cipher, hash_, + expected, monkeypatch): + module = DummyModule() + + module.params["device"] = device + module.params["keyfile"] = keyfile + module.params["passphrase"] = passphrase + module.params["state"] = state + module.params["label"] = label + module.params["cipher"] = cipher + module.params["hash"] = hash_ + + monkeypatch.setattr(luks_device.CryptHandler, "is_luks", + lambda x, y: is_luks) + crypt = luks_device.CryptHandler(module) + if device is None: + monkeypatch.setattr(luks_device.Handler, "get_device_by_label", + lambda x, y: [0, "/dev/dummy", ""]) + try: + conditions = luks_device.ConditionsHandler(module, crypt) + assert conditions.luks_create() == expected + except ValueError: + assert expected == "exception" + + +@pytest.mark.parametrize("device, state, is_luks, expected", + ((d[0], d[1], d[2], d[3]) + for d in LUKS_REMOVE_DATA)) +def test_luks_remove(device, state, is_luks, expected, monkeypatch): + module = DummyModule() + + module.params["device"] = device + module.params["state"] = state + + monkeypatch.setattr(luks_device.CryptHandler, "is_luks", + lambda x, y: is_luks) + crypt = luks_device.CryptHandler(module) + try: + conditions = luks_device.ConditionsHandler(module, crypt) + assert conditions.luks_remove() == expected + except ValueError: + assert expected == "exception" + + +@pytest.mark.parametrize("device, keyfile, passphrase, state, name, " + "name_by_dev, expected", + ((d[0], d[1], d[2], d[3], d[4], d[5], d[6]) + for d in LUKS_OPEN_DATA)) +def test_luks_open(device, keyfile, passphrase, state, name, name_by_dev, + expected, monkeypatch): + module = DummyModule() + module.params["device"] = device + module.params["keyfile"] = keyfile + module.params["passphrase"] = passphrase + module.params["state"] = state + module.params["name"] = name + + monkeypatch.setattr(luks_device.CryptHandler, + "get_container_name_by_device", + lambda x, y: name_by_dev) + monkeypatch.setattr(luks_device.CryptHandler, + "get_container_device_by_name", + lambda x, y: device) + monkeypatch.setattr(luks_device.Handler, "_run_command", + lambda x, y: [0, device, ""]) + crypt = luks_device.CryptHandler(module) + try: + conditions = luks_device.ConditionsHandler(module, crypt) + assert conditions.luks_open() == expected + except ValueError: + assert expected == "exception" + + +@pytest.mark.parametrize("device, dev_by_name, name, name_by_dev, " + "state, label, expected", + ((d[0], d[1], d[2], d[3], d[4], d[5], d[6]) + for d in LUKS_CLOSE_DATA)) +def test_luks_close(device, dev_by_name, name, name_by_dev, state, + label, expected, monkeypatch): + module = DummyModule() + module.params["device"] = device + module.params["name"] = name + module.params["state"] = state + module.params["label"] = label + + monkeypatch.setattr(luks_device.CryptHandler, + "get_container_name_by_device", + lambda x, y: name_by_dev) + monkeypatch.setattr(luks_device.CryptHandler, + "get_container_device_by_name", + lambda x, y: dev_by_name) + crypt = luks_device.CryptHandler(module) + try: + conditions = luks_device.ConditionsHandler(module, crypt) + assert conditions.luks_close() == expected + except ValueError: + assert expected == "exception" + + +@pytest.mark.parametrize("device, keyfile, passphrase, new_keyfile, " + + "new_passphrase, state, label, expected", + ((d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7]) + for d in LUKS_ADD_KEY_DATA)) +def test_luks_add_key(device, keyfile, passphrase, new_keyfile, new_passphrase, + state, label, expected, monkeypatch): + module = DummyModule() + module.params["device"] = device + module.params["keyfile"] = keyfile + module.params["passphrase"] = passphrase + module.params["new_keyfile"] = new_keyfile + module.params["new_passphrase"] = new_passphrase + module.params["state"] = state + module.params["label"] = label + + monkeypatch.setattr(luks_device.Handler, "get_device_by_label", + lambda x, y: [0, "/dev/dummy", ""]) + monkeypatch.setattr(luks_device.CryptHandler, "luks_test_key", + lambda x, y, z, w: False) + + crypt = luks_device.CryptHandler(module) + try: + conditions = luks_device.ConditionsHandler(module, crypt) + assert conditions.luks_add_key() == expected + except ValueError: + assert expected == "exception" + + +@pytest.mark.parametrize("device, remove_keyfile, remove_passphrase, state, " + + "label, expected", + ((d[0], d[1], d[2], d[3], d[4], d[5]) + for d in LUKS_REMOVE_KEY_DATA)) +def test_luks_remove_key(device, remove_keyfile, remove_passphrase, state, + label, expected, monkeypatch): + + module = DummyModule() + module.params["device"] = device + module.params["remove_keyfile"] = remove_keyfile + module.params["remove_passphrase"] = remove_passphrase + module.params["state"] = state + module.params["label"] = label + + monkeypatch.setattr(luks_device.Handler, "get_device_by_label", + lambda x, y: [0, "/dev/dummy", ""]) + monkeypatch.setattr(luks_device.Handler, "_run_command", + lambda x, y: [0, device, ""]) + monkeypatch.setattr(luks_device.CryptHandler, "luks_test_key", + lambda x, y, z, w: True) + + crypt = luks_device.CryptHandler(module) + try: + conditions = luks_device.ConditionsHandler(module, crypt) + assert conditions.luks_remove_key() == expected + except ValueError: + assert expected == "exception" diff --git a/ansible_collections/community/crypto/tests/unit/plugins/modules/utils.py b/ansible_collections/community/crypto/tests/unit/plugins/modules/utils.py new file mode 100644 index 000000000..a55a588a7 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/plugins/modules/utils.py @@ -0,0 +1,54 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import json + +from ansible_collections.community.crypto.tests.unit.compat import unittest +from ansible_collections.community.crypto.tests.unit.compat.mock import patch +from ansible.module_utils import basic +from ansible.module_utils.common.text.converters import to_bytes + + +def set_module_args(args): + if '_ansible_remote_tmp' not in args: + args['_ansible_remote_tmp'] = '/tmp' + if '_ansible_keep_remote_files' not in args: + args['_ansible_keep_remote_files'] = False + + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + + +class AnsibleExitJson(Exception): + pass + + +class AnsibleFailJson(Exception): + pass + + +def exit_json(*args, **kwargs): + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +class ModuleTestCase(unittest.TestCase): + + def setUp(self): + self.mock_module = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) + self.mock_module.start() + self.mock_sleep = patch('time.sleep') + self.mock_sleep.start() + set_module_args({}) + self.addCleanup(self.mock_module.stop) + self.addCleanup(self.mock_sleep.stop) diff --git a/ansible_collections/community/crypto/tests/unit/requirements.txt b/ansible_collections/community/crypto/tests/unit/requirements.txt new file mode 100644 index 000000000..0f2275f13 --- /dev/null +++ b/ansible_collections/community/crypto/tests/unit/requirements.txt @@ -0,0 +1,11 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +bcrypt +cryptography +idna +ipaddress ; python_version < '3.0' + +unittest2 ; python_version < '2.7' +importlib ; python_version < '2.7' diff --git a/ansible_collections/community/crypto/tests/utils/constraints.txt b/ansible_collections/community/crypto/tests/utils/constraints.txt new file mode 100644 index 000000000..a25c3b3ed --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/constraints.txt @@ -0,0 +1,17 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +coverage >= 4.2, < 5.0.0, != 4.3.2 ; python_version <= '3.7' # features in 4.2+ required, avoid known bug in 4.3.2 on python 2.6, coverage 5.0+ incompatible +coverage >= 4.5.4, < 5.0.0 ; python_version > '3.7' # coverage had a bug in < 4.5.4 that would cause unit tests to hang in Python 3.8, coverage 5.0+ incompatible +cryptography < 2.2 ; python_version < '2.7' # cryptography 2.2 drops support for python 2.6 +cryptography >= 3.0, < 3.4 ; python_version < '3.5' # cryptography 3.4 drops support for python 2.7 +cryptography >= 3.0, < 3.3 ; python_version == '3.5' # cryptography 3.3 drops support for python 3.5 +urllib3 < 1.24 ; python_version < '2.7' # urllib3 1.24 and later require python 2.7 or later +idna < 2.6, >= 2.5 # linode requires idna < 2.9, >= 2.5, requests requires idna < 2.6, but cryptography will cause the latest version to be installed instead +requests < 2.20.0 ; python_version < '2.7' # requests 2.20.0 drops support for python 2.6 +virtualenv < 16.0.0 ; python_version < '2.7' # virtualenv 16.0.0 and later require python 2.7 or later +pyopenssl < 18.0.0 ; python_version < '2.7' # pyOpenSSL 18.0.0 and later require python 2.7 or later +pyopenssl < 22.0.0 ; python_version < '3.6' # pyOpenSSL 22.0.0 and later require python 3.6 or later +setuptools < 45 ; python_version <= '2.7' # setuptools 45 and later require python 3.5 or later +cffi >= 1.14.2, != 1.14.3 # Yanked version which older versions of pip will still install: diff --git a/ansible_collections/community/crypto/tests/utils/shippable/alpine.sh b/ansible_collections/community/crypto/tests/utils/shippable/alpine.sh new file mode 100755 index 000000000..b70adf78c --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/alpine.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +platform="${args[0]}" +version="${args[1]}" +pyver=default + +# check for explicit python version like 8.3@3.8 +declare -a splitversion +IFS='@' read -ra splitversion <<< "$version" + +if [ "${#splitversion[@]}" -gt 1 ]; then + version="${splitversion[0]}" + pyver="${splitversion[1]}" +fi + +if [ "${#args[@]}" -gt 2 ]; then + target="azp/posix/${args[2]}/" +else + target="azp/posix/" +fi + +stage="${S:-prod}" +provider="${P:-default}" + +# shellcheck disable=SC2086 +ansible-test integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --python "${pyver}" --remote "${platform}/${version}" --remote-terminate always --remote-stage "${stage}" --remote-provider "${provider}" diff --git a/ansible_collections/community/crypto/tests/utils/shippable/fedora.sh b/ansible_collections/community/crypto/tests/utils/shippable/fedora.sh new file mode 100755 index 000000000..b70adf78c --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/fedora.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +platform="${args[0]}" +version="${args[1]}" +pyver=default + +# check for explicit python version like 8.3@3.8 +declare -a splitversion +IFS='@' read -ra splitversion <<< "$version" + +if [ "${#splitversion[@]}" -gt 1 ]; then + version="${splitversion[0]}" + pyver="${splitversion[1]}" +fi + +if [ "${#args[@]}" -gt 2 ]; then + target="azp/posix/${args[2]}/" +else + target="azp/posix/" +fi + +stage="${S:-prod}" +provider="${P:-default}" + +# shellcheck disable=SC2086 +ansible-test integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --python "${pyver}" --remote "${platform}/${version}" --remote-terminate always --remote-stage "${stage}" --remote-provider "${provider}" diff --git a/ansible_collections/community/crypto/tests/utils/shippable/freebsd.sh b/ansible_collections/community/crypto/tests/utils/shippable/freebsd.sh new file mode 100755 index 000000000..b70adf78c --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/freebsd.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +platform="${args[0]}" +version="${args[1]}" +pyver=default + +# check for explicit python version like 8.3@3.8 +declare -a splitversion +IFS='@' read -ra splitversion <<< "$version" + +if [ "${#splitversion[@]}" -gt 1 ]; then + version="${splitversion[0]}" + pyver="${splitversion[1]}" +fi + +if [ "${#args[@]}" -gt 2 ]; then + target="azp/posix/${args[2]}/" +else + target="azp/posix/" +fi + +stage="${S:-prod}" +provider="${P:-default}" + +# shellcheck disable=SC2086 +ansible-test integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --python "${pyver}" --remote "${platform}/${version}" --remote-terminate always --remote-stage "${stage}" --remote-provider "${provider}" diff --git a/ansible_collections/community/crypto/tests/utils/shippable/generic.sh b/ansible_collections/community/crypto/tests/utils/shippable/generic.sh new file mode 100755 index 000000000..34532791e --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/generic.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +python="${args[1]}" +group="${args[2]}" + +target="azp/generic/${group}/" + +stage="${S:-prod}" + +# shellcheck disable=SC2086 +export ANSIBLE_ACME_CONTAINER=quay.io/ansible/acme-test-container:2.0.0 # use new container until +ansible-test integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --remote-terminate always --remote-stage "${stage}" \ + --docker --python "${python}" diff --git a/ansible_collections/community/crypto/tests/utils/shippable/linux-community.sh b/ansible_collections/community/crypto/tests/utils/shippable/linux-community.sh new file mode 100755 index 000000000..48d0d8687 --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/linux-community.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +image="${args[1]}" +python="${args[2]}" + +if [ "${#args[@]}" -gt 3 ]; then + target="azp/posix/${args[3]}/" +else + target="azp/posix/" +fi + +# shellcheck disable=SC2086 +ansible-test integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --docker "quay.io/ansible-community/test-image:${image}" --python "${python}" diff --git a/ansible_collections/community/crypto/tests/utils/shippable/linux.sh b/ansible_collections/community/crypto/tests/utils/shippable/linux.sh new file mode 100755 index 000000000..6e1e2350b --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/linux.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +image="${args[1]}" + +if [ "${#args[@]}" -gt 2 ]; then + target="azp/posix/${args[2]}/" +else + target="azp/posix/" +fi + +# shellcheck disable=SC2086 +ansible-test integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --docker "${image}" diff --git a/ansible_collections/community/crypto/tests/utils/shippable/macos.sh b/ansible_collections/community/crypto/tests/utils/shippable/macos.sh new file mode 100755 index 000000000..b70adf78c --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/macos.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +platform="${args[0]}" +version="${args[1]}" +pyver=default + +# check for explicit python version like 8.3@3.8 +declare -a splitversion +IFS='@' read -ra splitversion <<< "$version" + +if [ "${#splitversion[@]}" -gt 1 ]; then + version="${splitversion[0]}" + pyver="${splitversion[1]}" +fi + +if [ "${#args[@]}" -gt 2 ]; then + target="azp/posix/${args[2]}/" +else + target="azp/posix/" +fi + +stage="${S:-prod}" +provider="${P:-default}" + +# shellcheck disable=SC2086 +ansible-test integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --python "${pyver}" --remote "${platform}/${version}" --remote-terminate always --remote-stage "${stage}" --remote-provider "${provider}" diff --git a/ansible_collections/community/crypto/tests/utils/shippable/osx.sh b/ansible_collections/community/crypto/tests/utils/shippable/osx.sh new file mode 100755 index 000000000..b70adf78c --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/osx.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +platform="${args[0]}" +version="${args[1]}" +pyver=default + +# check for explicit python version like 8.3@3.8 +declare -a splitversion +IFS='@' read -ra splitversion <<< "$version" + +if [ "${#splitversion[@]}" -gt 1 ]; then + version="${splitversion[0]}" + pyver="${splitversion[1]}" +fi + +if [ "${#args[@]}" -gt 2 ]; then + target="azp/posix/${args[2]}/" +else + target="azp/posix/" +fi + +stage="${S:-prod}" +provider="${P:-default}" + +# shellcheck disable=SC2086 +ansible-test integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --python "${pyver}" --remote "${platform}/${version}" --remote-terminate always --remote-stage "${stage}" --remote-provider "${provider}" diff --git a/ansible_collections/community/crypto/tests/utils/shippable/remote.sh b/ansible_collections/community/crypto/tests/utils/shippable/remote.sh new file mode 100755 index 000000000..b70adf78c --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/remote.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +platform="${args[0]}" +version="${args[1]}" +pyver=default + +# check for explicit python version like 8.3@3.8 +declare -a splitversion +IFS='@' read -ra splitversion <<< "$version" + +if [ "${#splitversion[@]}" -gt 1 ]; then + version="${splitversion[0]}" + pyver="${splitversion[1]}" +fi + +if [ "${#args[@]}" -gt 2 ]; then + target="azp/posix/${args[2]}/" +else + target="azp/posix/" +fi + +stage="${S:-prod}" +provider="${P:-default}" + +# shellcheck disable=SC2086 +ansible-test integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --python "${pyver}" --remote "${platform}/${version}" --remote-terminate always --remote-stage "${stage}" --remote-provider "${provider}" diff --git a/ansible_collections/community/crypto/tests/utils/shippable/rhel.sh b/ansible_collections/community/crypto/tests/utils/shippable/rhel.sh new file mode 100755 index 000000000..b70adf78c --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/rhel.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +platform="${args[0]}" +version="${args[1]}" +pyver=default + +# check for explicit python version like 8.3@3.8 +declare -a splitversion +IFS='@' read -ra splitversion <<< "$version" + +if [ "${#splitversion[@]}" -gt 1 ]; then + version="${splitversion[0]}" + pyver="${splitversion[1]}" +fi + +if [ "${#args[@]}" -gt 2 ]; then + target="azp/posix/${args[2]}/" +else + target="azp/posix/" +fi + +stage="${S:-prod}" +provider="${P:-default}" + +# shellcheck disable=SC2086 +ansible-test integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --python "${pyver}" --remote "${platform}/${version}" --remote-terminate always --remote-stage "${stage}" --remote-provider "${provider}" diff --git a/ansible_collections/community/crypto/tests/utils/shippable/sanity.sh b/ansible_collections/community/crypto/tests/utils/shippable/sanity.sh new file mode 100755 index 000000000..04b925bbb --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/sanity.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +group="${args[1]}" + +if [ "${BASE_BRANCH:-}" ]; then + base_branch="origin/${BASE_BRANCH}" +else + base_branch="" +fi + +if [ "${group}" == "extra" ]; then + ../internal_test_tools/tools/run.py --color --bot --junit + exit +fi + +# shellcheck disable=SC2086 +ansible-test sanity --color -v --junit ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} \ + --docker --base-branch "${base_branch}" \ + --allow-disabled diff --git a/ansible_collections/community/crypto/tests/utils/shippable/shippable.sh b/ansible_collections/community/crypto/tests/utils/shippable/shippable.sh new file mode 100755 index 000000000..526137698 --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/shippable.sh @@ -0,0 +1,239 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +ansible_version="${args[0]}" +script="${args[1]}" + +function join { + local IFS="$1"; + shift; + echo "$*"; +} + +# Ensure we can write other collections to this dir +sudo chown "$(whoami)" "${PWD}/../../" + +test="$(join / "${args[@]:1}")" +docker images ansible/ansible +docker images quay.io/ansible/* +docker ps + +for container in $(docker ps --format '{{.Image}} {{.ID}}' | grep -v -e '^drydock/' -e '^quay.io/ansible/azure-pipelines-test-container:' | sed 's/^.* //'); do + docker rm -f "${container}" || true # ignore errors +done + +docker ps + +if [ -d /home/shippable/cache/ ]; then + ls -la /home/shippable/cache/ +fi + +command -v python +python -V + +function retry +{ + # shellcheck disable=SC2034 + for repetition in 1 2 3; do + set +e + "$@" + result=$? + set -e + if [ ${result} == 0 ]; then + return ${result} + fi + echo "@* -> ${result}" + done + echo "Command '@*' failed 3 times!" + exit 255 +} + +command -v pip +pip --version +pip list --disable-pip-version-check +if [ "${ansible_version}" == "devel" ]; then + retry pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check +else + retry pip install "https://github.com/ansible/ansible/archive/stable-${ansible_version}.tar.gz" --disable-pip-version-check +fi + +# START: HACK +if [ "${script}" == "osx" ] && [ "${ansible_version}" == "2.9" ]; then + # Make sure that the latest versions of pyOpenSSL and cryptography will be installed on macOS before + # ansible-playbook is started. This is no longer necessary for devel (https://github.com/ansible/ansible/issues/68701 + # is fixed), but 2.9 still needs this since the new collection loader probably won't get backported to stable-2.9. + sed -i -e 's/cryptography.*/cryptography >= 2.9.2/g' /root/venv/lib/python2.7/site-packages/ansible_test/_data/requirements/integration.txt + echo 'pyOpenSSL >= 19.1.0' >> /root/venv/lib/python2.7/site-packages/ansible_test/_data/requirements/integration.txt +fi +# END: HACK + + +if [ "${SHIPPABLE_BUILD_ID:-}" ]; then + export ANSIBLE_COLLECTIONS_PATHS="${HOME}/.ansible" + SHIPPABLE_RESULT_DIR="$(pwd)/shippable" + TEST_DIR="${ANSIBLE_COLLECTIONS_PATHS}/ansible_collections/community/crypto" + mkdir -p "${TEST_DIR}" + cp -aT "${SHIPPABLE_BUILD_DIR}" "${TEST_DIR}" + cd "${TEST_DIR}" +else + # AZP + export ANSIBLE_COLLECTIONS_PATHS="$PWD/../../../" +fi + +if [ "${test}" == "sanity/extra" ]; then + retry pip install junit-xml --disable-pip-version-check +fi + +# START: HACK install integration test dependencies +if [ "${script}" != "units" ] && [ "${script}" != "sanity" ] || [ "${test}" == "sanity/extra" ]; then + # Nothing further should be added to this list. + # This is to prevent modules or plugins in this collection having a runtime dependency on other collections. + retry git clone --depth=1 --single-branch https://github.com/ansible-collections/community.internal_test_tools.git "${ANSIBLE_COLLECTIONS_PATHS}/ansible_collections/community/internal_test_tools" + # NOTE: we're installing with git to work around Galaxy being a huge PITA (https://github.com/ansible/galaxy/issues/2429) + # retry ansible-galaxy -vvv collection install community.internal_test_tools +fi + +if [ "${script}" != "units" ] && [ "${script}" != "sanity" ] && [ "${test}" != "sanity/extra" ] && [ "${ansible_version}" != "2.9" ]; then + retry git clone --depth=1 --single-branch https://github.com/ansible-collections/community.general.git "${ANSIBLE_COLLECTIONS_PATHS}/ansible_collections/community/general" + # NOTE: we're installing with git to work around Galaxy being a huge PITA (https://github.com/ansible/galaxy/issues/2429) + # retry ansible-galaxy -vvv collection install community.general +fi +# END: HACK + + +export PYTHONIOENCODING='utf-8' + +if [ "${JOB_TRIGGERED_BY_NAME:-}" == "nightly-trigger" ]; then + COVERAGE=yes + COMPLETE=yes +fi + +if [ -n "${COVERAGE:-}" ]; then + # on-demand coverage reporting triggered by setting the COVERAGE environment variable to a non-empty value + export COVERAGE="--coverage" +elif [[ "${COMMIT_MESSAGE}" =~ ci_coverage ]]; then + # on-demand coverage reporting triggered by having 'ci_coverage' in the latest commit message + export COVERAGE="--coverage" +else + # on-demand coverage reporting disabled (default behavior, always-on coverage reporting remains enabled) + export COVERAGE="--coverage-check" +fi + +if [ -n "${COMPLETE:-}" ]; then + # disable change detection triggered by setting the COMPLETE environment variable to a non-empty value + export CHANGED="" +elif [[ "${COMMIT_MESSAGE}" =~ ci_complete ]]; then + # disable change detection triggered by having 'ci_complete' in the latest commit message + export CHANGED="" +else + # enable change detection (default behavior) + export CHANGED="--changed" +fi + +if [ "${IS_PULL_REQUEST:-}" == "true" ]; then + # run unstable tests which are targeted by focused changes on PRs + export UNSTABLE="--allow-unstable-changed" +else + # do not run unstable tests outside PRs + export UNSTABLE="" +fi + +# remove empty core/extras module directories from PRs created prior to the repo-merge +find plugins -type d -empty -print -delete + +function cleanup +{ + # for complete on-demand coverage generate a report for all files with no coverage on the "sanity/5" job so we only have one copy + if [ "${COVERAGE}" == "--coverage" ] && [ "${CHANGED}" == "" ] && [ "${test}" == "sanity/5" ]; then + stub="--stub" + # trigger coverage reporting for stubs even if no other coverage data exists + mkdir -p tests/output/coverage/ + else + stub="" + fi + + if [ -d tests/output/coverage/ ]; then + if find tests/output/coverage/ -mindepth 1 -name '.*' -prune -o -print -quit | grep -q .; then + process_coverage='yes' # process existing coverage files + elif [ "${stub}" ]; then + process_coverage='yes' # process coverage when stubs are enabled + else + process_coverage='' + fi + + if [ "${process_coverage}" ]; then + # use python 3.7 for coverage to avoid running out of memory during coverage xml processing + # only use it for coverage to avoid the additional overhead of setting up a virtual environment for a potential no-op job + virtualenv --python /usr/bin/python3.7 ~/ansible-venv + set +ux + . ~/ansible-venv/bin/activate + set -ux + + # shellcheck disable=SC2086 + ansible-test coverage xml --color -v --requirements --group-by command --group-by version ${stub:+"$stub"} + cp -a tests/output/reports/coverage=*.xml "$SHIPPABLE_RESULT_DIR/codecoverage/" + + if [ "${ansible_version}" != "2.9" ]; then + # analyze and capture code coverage aggregated by integration test target + ansible-test coverage analyze targets generate -v "$SHIPPABLE_RESULT_DIR/testresults/coverage-analyze-targets.json" + fi + + # upload coverage report to codecov.io only when using complete on-demand coverage + if [ "${COVERAGE}" == "--coverage" ] && [ "${CHANGED}" == "" ]; then + for file in tests/output/reports/coverage=*.xml; do + flags="${file##*/coverage=}" + flags="${flags%-powershell.xml}" + flags="${flags%.xml}" + # remove numbered component from stub files when converting to tags + flags="${flags//stub-[0-9]*/stub}" + flags="${flags//=/,}" + flags="${flags//[^a-zA-Z0-9_,]/_}" + + bash <(curl -s https://ansible-ci-files.s3.us-east-1.amazonaws.com/codecov/codecov.sh) \ + -f "${file}" \ + -F "${flags}" \ + -n "${test}" \ + -t 31525df8-da26-4e61-b31f-05e3df48b091 \ + -X coveragepy \ + -X gcov \ + -X fix \ + -X search \ + -X xcode \ + || echo "Failed to upload code coverage report to codecov.io: ${file}" + done + fi + fi + fi + + if [ -d tests/output/junit/ ]; then + cp -aT tests/output/junit/ "$SHIPPABLE_RESULT_DIR/testresults/" + fi + + if [ -d tests/output/data/ ]; then + cp -a tests/output/data/ "$SHIPPABLE_RESULT_DIR/testresults/" + fi + + if [ -d tests/output/bot/ ]; then + cp -aT tests/output/bot/ "$SHIPPABLE_RESULT_DIR/testresults/" + fi +} + +if [ "${SHIPPABLE_BUILD_ID:-}" ]; then trap cleanup EXIT; fi + +if [[ "${COVERAGE:-}" == "--coverage" ]]; then + timeout=60 +else + timeout=50 +fi + +ansible-test env --dump --show --timeout "${timeout}" --color -v + +if [ "${SHIPPABLE_BUILD_ID:-}" ]; then "tests/utils/shippable/check_matrix.py"; fi +"tests/utils/shippable/${script}.sh" "${test}" diff --git a/ansible_collections/community/crypto/tests/utils/shippable/ubuntu.sh b/ansible_collections/community/crypto/tests/utils/shippable/ubuntu.sh new file mode 100755 index 000000000..b70adf78c --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/ubuntu.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +platform="${args[0]}" +version="${args[1]}" +pyver=default + +# check for explicit python version like 8.3@3.8 +declare -a splitversion +IFS='@' read -ra splitversion <<< "$version" + +if [ "${#splitversion[@]}" -gt 1 ]; then + version="${splitversion[0]}" + pyver="${splitversion[1]}" +fi + +if [ "${#args[@]}" -gt 2 ]; then + target="azp/posix/${args[2]}/" +else + target="azp/posix/" +fi + +stage="${S:-prod}" +provider="${P:-default}" + +# shellcheck disable=SC2086 +ansible-test integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --python "${pyver}" --remote "${platform}/${version}" --remote-terminate always --remote-stage "${stage}" --remote-provider "${provider}" diff --git a/ansible_collections/community/crypto/tests/utils/shippable/units.sh b/ansible_collections/community/crypto/tests/utils/shippable/units.sh new file mode 100755 index 000000000..bcf4d1820 --- /dev/null +++ b/ansible_collections/community/crypto/tests/utils/shippable/units.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +set -o pipefail -eux + +if [[ "${COVERAGE:-}" == "--coverage" ]]; then + timeout=90 +else + timeout=30 +fi + +ansible-test env --timeout "${timeout}" --color -v + +# shellcheck disable=SC2086 +ansible-test units --color -v --docker default ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} \ |