mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
606e1cd4da | ||
|
|
c7ef362d7a | ||
|
|
d8ccebce60 | ||
|
|
a7c06b2ec4 | ||
|
|
4f7ab6733d | ||
|
|
7714893294 | ||
|
|
d921ff1f68 | ||
|
|
3ca4c48b00 | ||
|
|
cd64bf8324 | ||
|
|
2031787506 | ||
|
|
d43998facf | ||
|
|
acab276d51 | ||
|
|
95040da881 | ||
|
|
ccb25eab36 | ||
|
|
fb2f3ef2b5 | ||
|
|
b2e13d3c03 | ||
|
|
b10e86a4ba | ||
|
|
04611d833d | ||
|
|
2c25719da5 | ||
|
|
3c7514f653 | ||
|
|
42e8279c75 | ||
|
|
5ba60e6f66 | ||
|
|
3fa229b7b3 | ||
|
|
52f7f0212b | ||
|
|
4d8dcad190 | ||
|
|
d7ad3e32d4 | ||
|
|
c9524e9c09 | ||
|
|
c2122acfba | ||
|
|
69335a8bac | ||
|
|
86b39733e1 | ||
|
|
94b23d62db | ||
|
|
942255923b | ||
|
|
68b45c2812 | ||
|
|
77d652da9a | ||
|
|
c7268c7fc4 | ||
|
|
5ffe97f874 | ||
|
|
ec7e4916e5 | ||
|
|
ec55161cb1 | ||
|
|
7559e912f1 | ||
|
|
92bc17463a | ||
|
|
fc4b5225d2 | ||
|
|
3c21079afa | ||
|
|
9792188b0e | ||
|
|
fd7871ae7d | ||
|
|
b32adcce78 | ||
|
|
a6490fa60e | ||
|
|
4fb3d09ee9 | ||
|
|
98db972a6c | ||
|
|
233d1afc29 | ||
|
|
7d0e5e814e | ||
|
|
42dd19c387 | ||
|
|
8e10e1e590 | ||
|
|
c645b7d732 | ||
|
|
010b54f0af | ||
|
|
7cdfdc1bfb | ||
|
|
1b3ff44bc2 | ||
|
|
7dd1f8a6bd |
3
.azure-pipelines/README.md
Normal file
3
.azure-pipelines/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## Azure Pipelines Configuration
|
||||
|
||||
Please see the [Documentation](https://github.com/ansible/community/wiki/Testing:-Azure-Pipelines) for more information.
|
||||
270
.azure-pipelines/azure-pipelines.yml
Normal file
270
.azure-pipelines/azure-pipelines.yml
Normal file
@@ -0,0 +1,270 @@
|
||||
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
|
||||
- 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:1.7.1
|
||||
|
||||
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_10
|
||||
displayName: Sanity & Units 2.10
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
targets:
|
||||
- name: Sanity
|
||||
test: '2.10/sanity/1'
|
||||
- name: Units
|
||||
test: '2.10/units/1'
|
||||
- stage: Ansible_2_9
|
||||
displayName: Sanity & Units 2.9
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
targets:
|
||||
- name: Sanity
|
||||
test: '2.9/sanity/1'
|
||||
- name: Units
|
||||
test: '2.9/units/1'
|
||||
### Docker
|
||||
- stage: Docker_devel
|
||||
displayName: Docker devel
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: devel/linux/{0}/1
|
||||
targets:
|
||||
- name: CentOS 6
|
||||
test: centos6
|
||||
- name: CentOS 7
|
||||
test: centos7
|
||||
- name: CentOS 8
|
||||
test: centos8
|
||||
- name: Fedora 30
|
||||
test: fedora30
|
||||
- name: Fedora 31
|
||||
test: fedora31
|
||||
- name: Fedora 32
|
||||
test: fedora32
|
||||
- name: openSUSE 15 py2
|
||||
test: opensuse15py2
|
||||
- name: openSUSE 15 py3
|
||||
test: opensuse15
|
||||
- name: Ubuntu 16.04
|
||||
test: ubuntu1604
|
||||
- name: Ubuntu 18.04
|
||||
test: ubuntu1804
|
||||
- name: Ubuntu 20.04
|
||||
test: ubuntu2004
|
||||
- stage: Docker_2_10
|
||||
displayName: Docker 2.10
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: 2.10/linux/{0}/1
|
||||
targets:
|
||||
- name: CentOS 6
|
||||
test: centos6
|
||||
- name: CentOS 7
|
||||
test: centos7
|
||||
- name: CentOS 8
|
||||
test: centos8
|
||||
- name: Fedora 30
|
||||
test: fedora30
|
||||
- name: Fedora 31
|
||||
test: fedora31
|
||||
- name: Fedora 32
|
||||
test: fedora32
|
||||
- name: openSUSE 15 py2
|
||||
test: opensuse15py2
|
||||
- name: openSUSE 15 py3
|
||||
test: opensuse15
|
||||
- name: Ubuntu 16.04
|
||||
test: ubuntu1604
|
||||
- name: Ubuntu 18.04
|
||||
test: ubuntu1804
|
||||
- stage: Docker_2_9
|
||||
displayName: Docker 2.9
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: 2.9/linux/{0}/1
|
||||
targets:
|
||||
- name: CentOS 6
|
||||
test: centos6
|
||||
- name: CentOS 7
|
||||
test: centos7
|
||||
- name: CentOS 8
|
||||
test: centos8
|
||||
- name: Fedora 30
|
||||
test: fedora30
|
||||
- name: Fedora 31
|
||||
test: fedora31
|
||||
# fedora32 doesn't exist in 2.9
|
||||
# - name: Fedora 32
|
||||
# test: fedora32
|
||||
- name: openSUSE 15 py2
|
||||
test: opensuse15py2
|
||||
- name: openSUSE 15 py3
|
||||
test: opensuse15
|
||||
- name: Ubuntu 16.04
|
||||
test: ubuntu1604
|
||||
- name: Ubuntu 18.04
|
||||
test: ubuntu1804
|
||||
|
||||
### Remote
|
||||
- stage: Remote_devel
|
||||
displayName: Remote devel
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: devel/{0}/1
|
||||
targets:
|
||||
- name: OS X 10.11
|
||||
test: osx/10.11
|
||||
- name: macOS 10.15
|
||||
test: macos/10.15
|
||||
- name: macOS 11.1
|
||||
test: macos/11.1
|
||||
- name: RHEL 7.8
|
||||
test: rhel/7.8
|
||||
- name: RHEL 8.2
|
||||
test: rhel/8.2
|
||||
- name: FreeBSD 11.4
|
||||
test: freebsd/11.4
|
||||
- name: FreeBSD 12.2
|
||||
test: freebsd/12.2
|
||||
- stage: Remote_2_10
|
||||
displayName: Remote 2.10
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: 2.10/{0}/1
|
||||
targets:
|
||||
- name: RHEL 7.8
|
||||
test: rhel/7.8
|
||||
- name: FreeBSD 12.1
|
||||
test: freebsd/12.1
|
||||
- stage: Remote_2_9
|
||||
displayName: Remote 2.9
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: 2.9/{0}/1
|
||||
targets:
|
||||
- name: 'RHEL 7.8'
|
||||
test: 'rhel/7.8'
|
||||
### cloud
|
||||
- stage: Cloud_devel
|
||||
displayName: Cloud devel
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
nameFormat: Python {0}
|
||||
testFormat: devel/cloud/{0}/1
|
||||
targets:
|
||||
- test: 2.6
|
||||
- test: 2.7
|
||||
- test: 3.5
|
||||
- test: 3.6
|
||||
- test: 3.7
|
||||
- test: 3.8
|
||||
- test: 3.9
|
||||
- stage: Cloud_2_10
|
||||
displayName: Cloud 2.10
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
nameFormat: Python {0}
|
||||
testFormat: 2.10/cloud/{0}/1
|
||||
targets:
|
||||
- test: 3.6
|
||||
- stage: Cloud_2_9
|
||||
displayName: Cloud 2.9
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
nameFormat: Python {0}
|
||||
testFormat: 2.9/cloud/{0}/1
|
||||
targets:
|
||||
- test: 3.5
|
||||
|
||||
## Finally
|
||||
|
||||
- stage: Summary
|
||||
condition: succeededOrFailed()
|
||||
dependsOn:
|
||||
- Ansible_devel
|
||||
- Ansible_2_10
|
||||
- Ansible_2_9
|
||||
- Remote_devel
|
||||
- Docker_devel
|
||||
- Cloud_devel
|
||||
- Remote_2_10
|
||||
- Docker_2_10
|
||||
- Cloud_2_10
|
||||
- Remote_2_9
|
||||
- Docker_2_9
|
||||
- Cloud_2_9
|
||||
jobs:
|
||||
- template: templates/coverage.yml
|
||||
20
.azure-pipelines/scripts/aggregate-coverage.sh
Executable file
20
.azure-pipelines/scripts/aggregate-coverage.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Aggregate code coverage results for later processing.
|
||||
|
||||
set -o pipefail -eu
|
||||
|
||||
agent_temp_directory="$1"
|
||||
|
||||
PATH="${PWD}/bin:${PATH}"
|
||||
|
||||
mkdir "${agent_temp_directory}/coverage/"
|
||||
|
||||
options=(--venv --venv-system-site-packages --color -v)
|
||||
|
||||
ansible-test coverage combine --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
|
||||
60
.azure-pipelines/scripts/combine-coverage.py
Executable file
60
.azure-pipelines/scripts/combine-coverage.py
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
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()
|
||||
24
.azure-pipelines/scripts/process-results.sh
Executable file
24
.azure-pipelines/scripts/process-results.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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
|
||||
27
.azure-pipelines/scripts/publish-codecov.sh
Executable file
27
.azure-pipelines/scripts/publish-codecov.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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.
|
||||
|
||||
set -o pipefail -eu
|
||||
|
||||
output_path="$1"
|
||||
|
||||
curl --silent --show-error https://codecov.io/bash > codecov.sh
|
||||
|
||||
for file in "${output_path}"/reports/coverage*.xml; do
|
||||
name="${file}"
|
||||
name="${name##*/}" # remove path
|
||||
name="${name##coverage=}" # remove 'coverage=' prefix if present
|
||||
name="${name%.xml}" # remove '.xml' suffix
|
||||
|
||||
bash codecov.sh \
|
||||
-f "${file}" \
|
||||
-n "${name}" \
|
||||
-X coveragepy \
|
||||
-X gcov \
|
||||
-X fix \
|
||||
-X search \
|
||||
-X xcode \
|
||||
|| echo "Failed to upload code coverage report to codecov.io: ${file}"
|
||||
done
|
||||
15
.azure-pipelines/scripts/report-coverage.sh
Executable file
15
.azure-pipelines/scripts/report-coverage.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate code coverage reports for uploading to Azure Pipelines and codecov.io.
|
||||
|
||||
set -o pipefail -eu
|
||||
|
||||
PATH="${PWD}/bin:${PATH}"
|
||||
|
||||
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 --stub --venv --venv-system-site-packages --color -v
|
||||
34
.azure-pipelines/scripts/run-tests.sh
Executable file
34
.azure-pipelines/scripts/run-tests.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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"
|
||||
25
.azure-pipelines/scripts/time-command.py
Executable file
25
.azure-pipelines/scripts/time-command.py
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
"""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()
|
||||
39
.azure-pipelines/templates/coverage.yml
Normal file
39
.azure-pipelines/templates/coverage.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
# 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.sh "$(outputPath)"
|
||||
displayName: Publish to codecov.io
|
||||
condition: gt(variables.coverageFileCount, 0)
|
||||
continueOnError: true
|
||||
55
.azure-pipelines/templates/matrix.yml
Normal file
55
.azure-pipelines/templates/matrix.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
# 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) }}
|
||||
45
.azure-pipelines/templates/test.yml
Normal file
45
.azure-pipelines/templates/test.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
# 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)"
|
||||
@@ -5,6 +5,91 @@ Community Crypto Release Notes
|
||||
.. contents:: Topics
|
||||
|
||||
|
||||
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
|
||||
======
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# Ansible Community Crypto Collection
|
||||
[](https://app.shippable.com/projects/5e66776ca27f990007073a42)
|
||||
|
||||
[](https://dev.azure.com/ansible/community.crypto/_build?definitionId=21)
|
||||
[](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/).
|
||||
|
||||
## Tested with Ansible
|
||||
|
||||
Tested with both the current Ansible 2.9 and 2.10 releases and the current development version of Ansible. Ansible versions before 2.9.10 are not supported.
|
||||
@@ -48,6 +51,8 @@ Most modules require a recent enough version of [the Python cryptography library
|
||||
- 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:
|
||||
|
||||
@@ -197,3 +197,125 @@ releases:
|
||||
- 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,6 +1,6 @@
|
||||
namespace: community
|
||||
name: crypto
|
||||
version: 1.1.1
|
||||
version: 1.4.0
|
||||
readme: README.md
|
||||
authors:
|
||||
- Ansible (github.com/ansible)
|
||||
@@ -21,7 +21,7 @@ tags:
|
||||
- openssh
|
||||
- pkcs12
|
||||
repository: https://github.com/ansible-collections/community.crypto
|
||||
#documentation: https://github.com/ansible-collection-migration/community.crypto/tree/main/docs
|
||||
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
|
||||
build_ignore:
|
||||
|
||||
@@ -23,3 +23,9 @@ plugin_routing:
|
||||
deprecation:
|
||||
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:
|
||||
redirect: community.crypto.crypto.pem
|
||||
deprecation:
|
||||
removal_version: 2.0.0
|
||||
warning_text: The 'crypto/identify.py' module_utils has been renamed 'crypto/pem.py'. Please update your imports
|
||||
|
||||
107
plugins/action/openssl_privatekey_pipe.py
Normal file
107
plugins/action/openssl_privatekey_pipe.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import base64
|
||||
|
||||
from ansible.module_utils._text 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))
|
||||
@@ -68,8 +68,8 @@ options:
|
||||
- "The ACME version of the endpoint."
|
||||
- "Must be 1 for the classic Let's Encrypt and Buypass ACME endpoints,
|
||||
or 2 for standardized ACME v2 endpoints."
|
||||
- "The default value is 1. Note that in Ansible 2.14, this option *will
|
||||
be required* and will no longer have a default."
|
||||
- "The default value is 1. Note that in community.crypto 2.0.0, this
|
||||
option *will be required* and will no longer have a default."
|
||||
- "Please also note that we will deprecate ACME v1 support eventually."
|
||||
type: int
|
||||
choices: [ 1, 2 ]
|
||||
@@ -81,8 +81,8 @@ options:
|
||||
server (for the ACME v1 protocol). This will create technically correct,
|
||||
but untrusted certificates."
|
||||
- "The default value is U(https://acme-staging.api.letsencrypt.org/directory).
|
||||
Note that in Ansible 2.14, this option *will be required* and will no longer
|
||||
have a default."
|
||||
Note that in community.crypto 2.0.0, this option *will be required* and
|
||||
will no longer have a default."
|
||||
- "For Let's Encrypt, all staging endpoints can be found here:
|
||||
U(https://letsencrypt.org/docs/staging-environment/). For Buypass, all
|
||||
endpoints can be found here:
|
||||
|
||||
587
plugins/doc_fragments/module_certificate.py
Normal file
587
plugins/doc_fragments/module_certificate.py
Normal file
@@ -0,0 +1,587 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
# Standard files documentation fragment
|
||||
DOCUMENTATION = r'''
|
||||
description:
|
||||
- This module allows one to (re)generate OpenSSL certificates.
|
||||
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL.
|
||||
- If both the cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
|
||||
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with C(select_crypto_backend)).
|
||||
Please note that the PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0.
|
||||
requirements:
|
||||
- PyOpenSSL >= 0.15 or cryptography >= 1.6 (if using C(selfsigned), C(ownca) or C(assertonly) provider)
|
||||
options:
|
||||
force:
|
||||
description:
|
||||
- Generate the certificate, even if it already exists.
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
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:
|
||||
- Path to 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
|
||||
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
|
||||
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(yes) results in an error.
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
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_ASSERTONLY_DOCUMENTATION = r'''
|
||||
description:
|
||||
- The C(assertonly) provider is intended for use cases where one is only interested in
|
||||
checking properties of a supplied certificate. Please note that this provider has been
|
||||
deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0. See the examples on how
|
||||
to emulate C(assertonly) usage with M(community.crypto.x509_certificate_info),
|
||||
M(community.crypto.openssl_csr_info), M(community.crypto.openssl_privatekey_info) and
|
||||
M(ansible.builtin.assert). This also allows more flexible checks than
|
||||
the ones offered by the C(assertonly) provider.
|
||||
- Many properties that can be specified in this module are for validation of an
|
||||
existing or newly generated certificate. The proper place to specify them, if you
|
||||
want to receive a certificate with these properties is a CSR (Certificate Signing Request).
|
||||
options:
|
||||
csr_path:
|
||||
description:
|
||||
- This is not required for the C(assertonly) provider.
|
||||
|
||||
csr_content:
|
||||
description:
|
||||
- This is not required for the C(assertonly) provider.
|
||||
|
||||
signature_algorithms:
|
||||
description:
|
||||
- A list of algorithms that you would accept the certificate to be signed with
|
||||
(e.g. ['sha256WithRSAEncryption', 'sha512WithRSAEncryption']).
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: list
|
||||
elements: str
|
||||
|
||||
issuer:
|
||||
description:
|
||||
- The key/value pairs that must be present in the issuer name field of the certificate.
|
||||
- If you need to specify more than one value with the same key, use a list as value.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: dict
|
||||
|
||||
issuer_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(issuer) field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
subject:
|
||||
description:
|
||||
- The key/value pairs that must be present in the subject name field of the certificate.
|
||||
- If you need to specify more than one value with the same key, use a list as value.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: dict
|
||||
|
||||
subject_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(subject) field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
has_expired:
|
||||
description:
|
||||
- Checks if the certificate is expired/not expired at the time the module is executed.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
version:
|
||||
description:
|
||||
- The version of the certificate.
|
||||
- Nowadays it should almost always be 3.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: int
|
||||
|
||||
valid_at:
|
||||
description:
|
||||
- The certificate must be valid at this point in time.
|
||||
- The timestamp is formatted as an ASN.1 TIME.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
|
||||
invalid_at:
|
||||
description:
|
||||
- The certificate must be invalid at this point in time.
|
||||
- The timestamp is formatted as an ASN.1 TIME.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
|
||||
not_before:
|
||||
description:
|
||||
- The certificate must start to become valid at this point in time.
|
||||
- The timestamp is formatted as an ASN.1 TIME.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
aliases: [ notBefore ]
|
||||
|
||||
not_after:
|
||||
description:
|
||||
- The certificate must expire at this point in time.
|
||||
- The timestamp is formatted as an ASN.1 TIME.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
aliases: [ notAfter ]
|
||||
|
||||
valid_in:
|
||||
description:
|
||||
- The certificate must still be valid at this relative time offset from now.
|
||||
- Valid format is C([+-]timespec | number_of_seconds) where timespec can be an integer
|
||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
||||
- Note that if using this parameter, this module is NOT idempotent.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
|
||||
key_usage:
|
||||
description:
|
||||
- The I(key_usage) extension field must contain all these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ keyUsage ]
|
||||
|
||||
key_usage_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(key_usage) extension field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
aliases: [ keyUsage_strict ]
|
||||
|
||||
extended_key_usage:
|
||||
description:
|
||||
- The I(extended_key_usage) extension field must contain all these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ extendedKeyUsage ]
|
||||
|
||||
extended_key_usage_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(extended_key_usage) extension field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
aliases: [ extendedKeyUsage_strict ]
|
||||
|
||||
subject_alt_name:
|
||||
description:
|
||||
- The I(subject_alt_name) extension field must contain these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ subjectAltName ]
|
||||
|
||||
subject_alt_name_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(subject_alt_name) extension field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
aliases: [ subjectAltName_strict ]
|
||||
'''
|
||||
|
||||
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.
|
||||
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]) (e.g. C(+32w1d2h).
|
||||
- Note that if using relative time this module is NOT idempotent.
|
||||
- If this value is not specified, the certificate will start being valid from now.
|
||||
- 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]) (e.g. C(+32w1d2h).
|
||||
- Note that if using relative time this module is NOT idempotent.
|
||||
- If this value is not specified, the certificate will stop being valid 10 years from now.
|
||||
- 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: yes
|
||||
'''
|
||||
|
||||
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]) (e.g. C(+32w1d2h).
|
||||
- Note that if using relative time this module is NOT idempotent.
|
||||
- If this value is not specified, the certificate will start being valid from now.
|
||||
- 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]) (e.g. C(+32w1d2h).
|
||||
- Note that if using relative time this module is NOT idempotent.
|
||||
- If this value is not specified, the certificate will stop being valid 10 years from now.
|
||||
- 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
|
||||
'''
|
||||
318
plugins/doc_fragments/module_csr.py
Normal file
318
plugins/doc_fragments/module_csr.py
Normal file
@@ -0,0 +1,318 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyrigt: (c) 2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
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.
|
||||
- "The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the
|
||||
PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
|
||||
requirements:
|
||||
- Either cryptography >= 1.3
|
||||
- Or pyOpenSSL >= 0.15
|
||||
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 will no longer accept unsupported values from community.crypto 2.0.0 on.
|
||||
type: int
|
||||
default: 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.
|
||||
type: dict
|
||||
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.
|
||||
- This can either be a 'comma separated string' or a YAML list.
|
||||
- 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).
|
||||
- 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(yes), 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: yes
|
||||
aliases: [ useCommonNameForSAN ]
|
||||
key_usage:
|
||||
description:
|
||||
- This defines the purpose (e.g. 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 (e.g. 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 *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, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
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: no
|
||||
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(no).
|
||||
- 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)"
|
||||
- If specified, I(authority_cert_issuer) 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) 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_key_identifier) 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) 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.
|
||||
- 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) 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
|
||||
'''
|
||||
163
plugins/doc_fragments/module_privatekey.py
Normal file
163
plugins/doc_fragments/module_privatekey.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
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 don't match
|
||||
the module's options. In particular, if you provide another passphrase
|
||||
(or specify none), change the keysize, etc., the private key will be
|
||||
regenerated. If you are concerned that this could **overwrite your private key**,
|
||||
consider using the I(backup) option."
|
||||
- "The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the
|
||||
PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
|
||||
requirements:
|
||||
- Either cryptography >= 1.2.3 (older versions might work as well)
|
||||
- Or pyOpenSSL
|
||||
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. (Valid values can be found by
|
||||
running `openssl list -cipher-algorithms` or `openssl list-cipher-algorithms`,
|
||||
depending on your OpenSSL version.)
|
||||
- When using the C(cryptography) backend, use C(auto).
|
||||
type: str
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
format:
|
||||
description:
|
||||
- Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format)
|
||||
is used for all keys which support it. Please note that not every key can be exported in any format.
|
||||
- The value C(auto) selects a fromat based on the key format. The value C(auto_ignore) does the same,
|
||||
but for existing private key files, it will not force a regenerate when its format is not the automatically
|
||||
selected one for generation.
|
||||
- Note that if the format for an existing private key mismatches, the key is *regenerated* by default.
|
||||
To change this behavior, use the I(format_mismatch) option.
|
||||
- The I(format) option is only supported by the C(cryptography) backend. The C(pyopenssl) backend will
|
||||
fail if a value different from C(auto_ignore) is used.
|
||||
type: str
|
||||
default: auto_ignore
|
||||
choices: [ pkcs1, pkcs8, raw, auto, auto_ignore ]
|
||||
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 doesn't match the module's options,
|
||||
except when the key cannot be read or the passphrase does not match. Please note that
|
||||
this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence)
|
||||
is specified.
|
||||
- If set to C(never), the module will fail if the key cannot be read or the passphrase
|
||||
isn't matching, and will never regenerate an existing key.
|
||||
- If set to C(fail), the module will fail if the key does not correspond to the module's
|
||||
options.
|
||||
- If set to C(partial_idempotence), the key will be regenerated if it does not conform to
|
||||
the module's options. The key is B(not) regenerated if it cannot be read (broken file),
|
||||
the key is protected by an unknown passphrase, or when they key is not protected by a
|
||||
passphrase, but a passphrase is specified.
|
||||
- If set to C(full_idempotence), the key will be regenerated if it does not conform to the
|
||||
module's options. This is also the case if the key cannot be read (broken file), the key
|
||||
is protected by an unknown passphrase, or when they key is not protected by a passphrase,
|
||||
but a passphrase is specified. Make sure you have a B(backup) when using this option!
|
||||
- If set to C(always), the module will always regenerate the key. This is equivalent to
|
||||
setting I(force) to C(yes).
|
||||
- Note that if I(format_mismatch) is set to C(convert) and everything matches except the
|
||||
format, the key will always be converted, except if I(regenerate) is set to C(always).
|
||||
type: str
|
||||
choices:
|
||||
- never
|
||||
- fail
|
||||
- partial_idempotence
|
||||
- full_idempotence
|
||||
- always
|
||||
default: full_idempotence
|
||||
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
|
||||
'''
|
||||
@@ -1,14 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This code is part of Ansible, but is an independent component.
|
||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
||||
# 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), Michael Gruener <michael.gruener@chaosmoon.net>, 2016
|
||||
#
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
# (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
@@ -140,27 +133,31 @@ def write_file(module, dest, content):
|
||||
return changed
|
||||
|
||||
|
||||
def pem_to_der(pem_filename):
|
||||
def pem_to_der(pem_filename, pem_content=None):
|
||||
'''
|
||||
Load PEM file, and convert to DER.
|
||||
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 = []
|
||||
try:
|
||||
with open(pem_filename, "rt") as f:
|
||||
header_line_count = 0
|
||||
for line in f:
|
||||
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())
|
||||
except Exception as err:
|
||||
raise ModuleFailException("cannot load PEM file {0}: {1}".format(pem_filename, to_native(err)), exception=traceback.format_exc())
|
||||
if pem_content is not None:
|
||||
lines = pem_content.splitlines()
|
||||
else:
|
||||
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())
|
||||
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))
|
||||
|
||||
|
||||
@@ -989,14 +986,20 @@ def _normalize_ip(ip):
|
||||
return ip
|
||||
|
||||
|
||||
def openssl_get_csr_identifiers(openssl_binary, module, csr_filename):
|
||||
def openssl_get_csr_identifiers(openssl_binary, module, csr_filename, 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'.
|
||||
'''
|
||||
openssl_csr_cmd = [openssl_binary, "req", "-in", csr_filename, "-noout", "-text"]
|
||||
dummy, out, dummy = module.run_command(openssl_csr_cmd, check_rc=True)
|
||||
filename = csr_filename
|
||||
data = None
|
||||
if csr_content is not None:
|
||||
filename = '-'
|
||||
data = csr_content.encode('utf-8')
|
||||
|
||||
openssl_csr_cmd = [openssl_binary, "req", "-in", filename, "-noout", "-text"]
|
||||
dummy, out, dummy = module.run_command(openssl_csr_cmd, data=data, check_rc=True)
|
||||
|
||||
identifiers = set([])
|
||||
common_name = re.search(r"Subject:.* CN\s?=\s?([^\s,;/]+)", to_text(out, errors='surrogate_or_strict'))
|
||||
@@ -1018,14 +1021,18 @@ def openssl_get_csr_identifiers(openssl_binary, module, csr_filename):
|
||||
return identifiers
|
||||
|
||||
|
||||
def cryptography_get_csr_identifiers(module, csr_filename):
|
||||
def cryptography_get_csr_identifiers(module, csr_filename, 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([])
|
||||
csr = cryptography.x509.load_pem_x509_csr(read_file(csr_filename), _cryptography_backend)
|
||||
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))
|
||||
|
||||
@@ -55,7 +55,7 @@ from .cryptography_support import (
|
||||
cryptography_compare_public_keys,
|
||||
)
|
||||
|
||||
from .identify import (
|
||||
from .pem import (
|
||||
identify_private_key_format,
|
||||
)
|
||||
|
||||
|
||||
@@ -152,6 +152,36 @@ def _parse_hex(bytesstr):
|
||||
return data
|
||||
|
||||
|
||||
DN_COMPONENT_START_RE = re.compile(r'^ *([a-zA-z0-9]+) *= *')
|
||||
|
||||
|
||||
def _parse_dn_component(name, sep=',', sep_str='\\', decode_remainder=True):
|
||||
m = DN_COMPONENT_START_RE.match(name)
|
||||
if not m:
|
||||
raise OpenSSLObjectError('cannot start part in "{0}"'.format(name))
|
||||
oid = cryptography_name_to_oid(m.group(1))
|
||||
idx = len(m.group(0))
|
||||
decoded_name = []
|
||||
if decode_remainder:
|
||||
length = len(name)
|
||||
while idx < length:
|
||||
i = idx
|
||||
while i < length and name[i] not in sep_str:
|
||||
i += 1
|
||||
if i > idx:
|
||||
decoded_name.append(name[idx:i])
|
||||
idx = i
|
||||
while idx + 1 < length and name[idx] == '\\':
|
||||
decoded_name.append(name[idx + 1])
|
||||
idx += 2
|
||||
if idx < length and name[idx] == sep:
|
||||
break
|
||||
else:
|
||||
decoded_name.append(name[idx:])
|
||||
idx = len(name)
|
||||
return x509.NameAttribute(oid, ''.join(decoded_name)), name[idx:]
|
||||
|
||||
|
||||
def _parse_dn(name):
|
||||
'''
|
||||
Parse a Distinguished Name.
|
||||
@@ -166,29 +196,12 @@ def _parse_dn(name):
|
||||
name = name[1:]
|
||||
sep_str = sep + '\\'
|
||||
result = []
|
||||
start_re = re.compile(r'^ *([a-zA-z0-9]+) *= *')
|
||||
while name:
|
||||
m = start_re.match(name)
|
||||
if not m:
|
||||
raise OpenSSLObjectError('Error while parsing distinguished name "{0}": cannot start part in "{1}"'.format(original_name, name))
|
||||
oid = cryptography_name_to_oid(m.group(1))
|
||||
idx = len(m.group(0))
|
||||
decoded_name = []
|
||||
length = len(name)
|
||||
while idx < length:
|
||||
i = idx
|
||||
while i < length and name[i] not in sep_str:
|
||||
i += 1
|
||||
if i > idx:
|
||||
decoded_name.append(name[idx:i])
|
||||
idx = i
|
||||
while idx + 1 < length and name[idx] == '\\':
|
||||
decoded_name.append(name[idx + 1])
|
||||
idx += 2
|
||||
if idx < length and name[idx] == sep:
|
||||
break
|
||||
result.append(x509.NameAttribute(oid, ''.join(decoded_name)))
|
||||
name = name[idx:]
|
||||
try:
|
||||
attribute, name = _parse_dn_component(name, sep=sep, sep_str=sep_str)
|
||||
except OpenSSLObjectError as e:
|
||||
raise OpenSSLObjectError('Error while parsing distinguished name "{0}": {1}'.format(original_name, e))
|
||||
result.append(attribute)
|
||||
if name:
|
||||
if name[0] != sep or len(name) < 2:
|
||||
raise OpenSSLObjectError('Error while parsing distinguished name "{0}": unexpected end of string'.format(original_name))
|
||||
@@ -196,9 +209,19 @@ def _parse_dn(name):
|
||||
return result
|
||||
|
||||
|
||||
def cryptography_get_name(name):
|
||||
def cryptography_parse_relative_distinguished_name(rdn):
|
||||
names = []
|
||||
for part in rdn:
|
||||
try:
|
||||
names.append(_parse_dn_component(to_text(part), decode_remainder=False)[0])
|
||||
except OpenSSLObjectError as e:
|
||||
raise OpenSSLObjectError('Error while parsing relative distinguished name "{0}": {1}'.format(part, e))
|
||||
return cryptography.x509.RelativeDistinguishedName(names)
|
||||
|
||||
|
||||
def cryptography_get_name(name, what='Subject Alternative Name'):
|
||||
'''
|
||||
Given a name string, returns a cryptography x509.Name object.
|
||||
Given a name string, returns a cryptography x509.GeneralName object.
|
||||
Raises an OpenSSLObjectError if the name is unknown or cannot be parsed.
|
||||
'''
|
||||
try:
|
||||
@@ -216,7 +239,7 @@ def cryptography_get_name(name):
|
||||
if name.startswith('RID:'):
|
||||
m = re.match(r'^([0-9]+(?:\.[0-9]+)*)$', to_text(name[4:]))
|
||||
if not m:
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}"'.format(name))
|
||||
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.
|
||||
@@ -228,9 +251,9 @@ def cryptography_get_name(name):
|
||||
# defailts on the format expected.
|
||||
name = to_text(name[10:], errors='surrogate_or_strict')
|
||||
if ';' not in name:
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name otherName "{0}", must be in the '
|
||||
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))
|
||||
'"otherName:<OID>;<hex string>"'.format(name=name, what=what))
|
||||
|
||||
oid, value = name.split(';', 1)
|
||||
b_value = serialize_asn1_string_as_der(value)
|
||||
@@ -238,10 +261,10 @@ def cryptography_get_name(name):
|
||||
if name.startswith('dirName:'):
|
||||
return x509.DirectoryName(x509.Name(_parse_dn(to_text(name[8:]))))
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}": {1}'.format(name, e))
|
||||
raise OpenSSLObjectError('Cannot parse {what} "{name}": {error}'.format(name=name, what=what, error=e))
|
||||
if ':' not in name:
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (forgot "DNS:" prefix?)'.format(name))
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (potentially unsupported by cryptography backend)'.format(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):
|
||||
@@ -258,7 +281,7 @@ def _dn_escape_value(value):
|
||||
|
||||
def cryptography_decode_name(name):
|
||||
'''
|
||||
Given a cryptography x509.Name object, returns a string.
|
||||
Given a cryptography x509.GeneralName object, returns a string.
|
||||
Raises an OpenSSLObjectError if the name is not supported.
|
||||
'''
|
||||
if isinstance(name, x509.DNSName):
|
||||
|
||||
392
plugins/module_utils/crypto/module_backends/certificate.py
Normal file
392
plugins/module_utils/crypto/module_backends/certificate.py
Normal file
@@ -0,0 +1,392 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
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.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
|
||||
|
||||
@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
|
||||
|
||||
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 == 'pyopenssl':
|
||||
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD)
|
||||
ctx.use_privatekey(self.privatekey)
|
||||
ctx.use_certificate(self.existing_certificate)
|
||||
try:
|
||||
ctx.check_privatekey()
|
||||
return True
|
||||
except OpenSSL.SSL.Error:
|
||||
return False
|
||||
elif 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 == 'pyopenssl':
|
||||
# Verify that CSR is signed by certificate's private key
|
||||
try:
|
||||
self.csr.verify(self.existing_certificate.get_pubkey())
|
||||
except OpenSSL.crypto.Error:
|
||||
return False
|
||||
# Check subject
|
||||
if self.check_csr_subject and self.csr.get_subject() != self.existing_certificate.get_subject():
|
||||
return False
|
||||
# Check extensions
|
||||
if not self.check_csr_extensions:
|
||||
return True
|
||||
csr_extensions = self.csr.get_extensions()
|
||||
cert_extension_count = self.existing_certificate.get_extension_count()
|
||||
if len(csr_extensions) != cert_extension_count:
|
||||
return False
|
||||
for extension_number in range(0, cert_extension_count):
|
||||
cert_extension = self.existing_certificate.get_extension(extension_number)
|
||||
csr_extension = filter(lambda extension: extension.get_short_name() == cert_extension.get_short_name(), csr_extensions)
|
||||
if cert_extension.get_data() != list(csr_extension)[0].get_data():
|
||||
return False
|
||||
return True
|
||||
elif 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."""
|
||||
if self.backend != 'cryptography':
|
||||
# We do not support SKI with pyOpenSSL backend
|
||||
return True
|
||||
|
||||
# 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 didn't ignore it ('create_if_not_provided'), compare SKIs
|
||||
if ext.value.digest != csr_ext.value.digest:
|
||||
return False
|
||||
return True
|
||||
|
||||
def needs_regeneration(self):
|
||||
"""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
|
||||
|
||||
return False
|
||||
|
||||
def dump(self, include_certificate):
|
||||
"""Serialize the object into a dictionary."""
|
||||
result = {
|
||||
'privatekey': self.privatekey_path,
|
||||
'csr': self.csr_path
|
||||
}
|
||||
if include_certificate:
|
||||
# Get hold of certificate bytes
|
||||
certificate_bytes = self.existing_certificate_bytes
|
||||
if self.cert is not None:
|
||||
certificate_bytes = self.get_certificate_data()
|
||||
# Store result
|
||||
result['certificate'] = certificate_bytes.decode('utf-8') if certificate_bytes else None
|
||||
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."""
|
||||
|
||||
def needs_pyopenssl_get_extensions(self, module):
|
||||
"""Whether the provider needs to use get_extensions() with pyOpenSSL."""
|
||||
return True
|
||||
|
||||
@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)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# If cryptography is available we'll use it
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
if provider.needs_version_two_certs(module):
|
||||
module.warn('crypto backend forced to pyopenssl. The cryptography library does not support v2 certificates')
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Fail if no backend has been found
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
if provider.needs_pyopenssl_get_extensions(module):
|
||||
try:
|
||||
getattr(crypto.X509Req, 'get_extensions')
|
||||
except AttributeError:
|
||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15')
|
||||
|
||||
elif 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, '
|
||||
'use select_crypto_backend=pyopenssl for 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'),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
|
||||
# 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'],
|
||||
],
|
||||
)
|
||||
119
plugins/module_utils/crypto/module_backends/certificate_acme.py
Normal file
119
plugins/module_utils/crypto/module_backends/certificate_acme.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils._text 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"),
|
||||
))
|
||||
@@ -0,0 +1,664 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import datetime
|
||||
|
||||
from ansible.module_utils._text import to_native, to_bytes, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
parse_name_field,
|
||||
get_relative_time_option,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_compare_public_keys,
|
||||
cryptography_get_name,
|
||||
cryptography_name_to_oid,
|
||||
cryptography_parse_key_usage_params,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
|
||||
pyopenssl_normalize_name_attribute,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
CertificateBackend,
|
||||
CertificateProvider,
|
||||
)
|
||||
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.x509 import NameAttribute, Name
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def compare_sets(subset, superset, equality=False):
|
||||
if equality:
|
||||
return set(subset) == set(superset)
|
||||
else:
|
||||
return all(x in superset for x in subset)
|
||||
|
||||
|
||||
def compare_dicts(subset, superset, equality=False):
|
||||
if equality:
|
||||
return subset == superset
|
||||
else:
|
||||
return all(superset.get(x) == v for x, v in subset.items())
|
||||
|
||||
|
||||
NO_EXTENSION = 'no extension'
|
||||
|
||||
|
||||
class AssertOnlyCertificateBackend(CertificateBackend):
|
||||
def __init__(self, module, backend):
|
||||
super(AssertOnlyCertificateBackend, self).__init__(module, backend)
|
||||
|
||||
self.signature_algorithms = module.params['signature_algorithms']
|
||||
if module.params['subject']:
|
||||
self.subject = parse_name_field(module.params['subject'])
|
||||
else:
|
||||
self.subject = []
|
||||
self.subject_strict = module.params['subject_strict']
|
||||
if module.params['issuer']:
|
||||
self.issuer = parse_name_field(module.params['issuer'])
|
||||
else:
|
||||
self.issuer = []
|
||||
self.issuer_strict = module.params['issuer_strict']
|
||||
self.has_expired = module.params['has_expired']
|
||||
self.version = module.params['version']
|
||||
self.key_usage = module.params['key_usage']
|
||||
self.key_usage_strict = module.params['key_usage_strict']
|
||||
self.extended_key_usage = module.params['extended_key_usage']
|
||||
self.extended_key_usage_strict = module.params['extended_key_usage_strict']
|
||||
self.subject_alt_name = module.params['subject_alt_name']
|
||||
self.subject_alt_name_strict = module.params['subject_alt_name_strict']
|
||||
self.not_before = module.params['not_before']
|
||||
self.not_after = module.params['not_after']
|
||||
self.valid_at = module.params['valid_at']
|
||||
self.invalid_at = module.params['invalid_at']
|
||||
self.valid_in = module.params['valid_in']
|
||||
if self.valid_in and not self.valid_in.startswith("+") and not self.valid_in.startswith("-"):
|
||||
try:
|
||||
int(self.valid_in)
|
||||
except ValueError:
|
||||
module.fail_json(msg='The supplied value for "valid_in" (%s) is not an integer or a valid timespec' % self.valid_in)
|
||||
self.valid_in = "+" + self.valid_in + "s"
|
||||
|
||||
# Load objects
|
||||
self._ensure_private_key_loaded()
|
||||
self._ensure_csr_loaded()
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_privatekey(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_csr_signature(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_csr_subject(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_csr_extensions(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_signature_algorithms(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_subject(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_issuer(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_has_expired(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_version(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_extended_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_subject_alt_name(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_not_before(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_not_after(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_valid_at(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_invalid_at(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_valid_in(self):
|
||||
pass
|
||||
|
||||
def assertonly(self):
|
||||
messages = []
|
||||
if self.privatekey_path is not None or self.privatekey_content is not None:
|
||||
if not self._validate_privatekey():
|
||||
messages.append(
|
||||
'Certificate %s and private key %s do not match' %
|
||||
(self.path, self.privatekey_path or '(provided in module options)')
|
||||
)
|
||||
|
||||
if self.csr_path is not None or self.csr_content is not None:
|
||||
if not self._validate_csr_signature():
|
||||
messages.append(
|
||||
'Certificate %s and CSR %s do not match: private key mismatch' %
|
||||
(self.path, self.csr_path or '(provided in module options)')
|
||||
)
|
||||
if not self._validate_csr_subject():
|
||||
messages.append(
|
||||
'Certificate %s and CSR %s do not match: subject mismatch' %
|
||||
(self.path, self.csr_path or '(provided in module options)')
|
||||
)
|
||||
if not self._validate_csr_extensions():
|
||||
messages.append(
|
||||
'Certificate %s and CSR %s do not match: extensions mismatch' %
|
||||
(self.path, self.csr_path or '(provided in module options)')
|
||||
)
|
||||
|
||||
if self.signature_algorithms is not None:
|
||||
wrong_alg = self._validate_signature_algorithms()
|
||||
if wrong_alg:
|
||||
messages.append(
|
||||
'Invalid signature algorithm (got %s, expected one of %s)' %
|
||||
(wrong_alg, self.signature_algorithms)
|
||||
)
|
||||
|
||||
if self.subject is not None:
|
||||
failure = self._validate_subject()
|
||||
if failure:
|
||||
dummy, cert_subject = failure
|
||||
messages.append(
|
||||
'Invalid subject component (got %s, expected all of %s to be present)' %
|
||||
(cert_subject, self.subject)
|
||||
)
|
||||
|
||||
if self.issuer is not None:
|
||||
failure = self._validate_issuer()
|
||||
if failure:
|
||||
dummy, cert_issuer = failure
|
||||
messages.append(
|
||||
'Invalid issuer component (got %s, expected all of %s to be present)' % (cert_issuer, self.issuer)
|
||||
)
|
||||
|
||||
if self.has_expired is not None:
|
||||
cert_expired = self._validate_has_expired()
|
||||
if cert_expired != self.has_expired:
|
||||
messages.append(
|
||||
'Certificate expiration check failed (certificate expiration is %s, expected %s)' %
|
||||
(cert_expired, self.has_expired)
|
||||
)
|
||||
|
||||
if self.version is not None:
|
||||
cert_version = self._validate_version()
|
||||
if cert_version != self.version:
|
||||
messages.append(
|
||||
'Invalid certificate version number (got %s, expected %s)' %
|
||||
(cert_version, self.version)
|
||||
)
|
||||
|
||||
if self.key_usage is not None:
|
||||
failure = self._validate_key_usage()
|
||||
if failure == NO_EXTENSION:
|
||||
messages.append('Found no keyUsage extension')
|
||||
elif failure:
|
||||
dummy, cert_key_usage = failure
|
||||
messages.append(
|
||||
'Invalid keyUsage components (got %s, expected all of %s to be present)' %
|
||||
(cert_key_usage, self.key_usage)
|
||||
)
|
||||
|
||||
if self.extended_key_usage is not None:
|
||||
failure = self._validate_extended_key_usage()
|
||||
if failure == NO_EXTENSION:
|
||||
messages.append('Found no extendedKeyUsage extension')
|
||||
elif failure:
|
||||
dummy, ext_cert_key_usage = failure
|
||||
messages.append(
|
||||
'Invalid extendedKeyUsage component (got %s, expected all of %s to be present)' % (ext_cert_key_usage, self.extended_key_usage)
|
||||
)
|
||||
|
||||
if self.subject_alt_name is not None:
|
||||
failure = self._validate_subject_alt_name()
|
||||
if failure == NO_EXTENSION:
|
||||
messages.append('Found no subjectAltName extension')
|
||||
elif failure:
|
||||
dummy, cert_san = failure
|
||||
messages.append(
|
||||
'Invalid subjectAltName component (got %s, expected all of %s to be present)' %
|
||||
(cert_san, self.subject_alt_name)
|
||||
)
|
||||
|
||||
if self.not_before is not None:
|
||||
cert_not_valid_before = self._validate_not_before()
|
||||
if cert_not_valid_before != get_relative_time_option(self.not_before, 'not_before', backend=self.backend):
|
||||
messages.append(
|
||||
'Invalid not_before component (got %s, expected %s to be present)' %
|
||||
(cert_not_valid_before, self.not_before)
|
||||
)
|
||||
|
||||
if self.not_after is not None:
|
||||
cert_not_valid_after = self._validate_not_after()
|
||||
if cert_not_valid_after != get_relative_time_option(self.not_after, 'not_after', backend=self.backend):
|
||||
messages.append(
|
||||
'Invalid not_after component (got %s, expected %s to be present)' %
|
||||
(cert_not_valid_after, self.not_after)
|
||||
)
|
||||
|
||||
if self.valid_at is not None:
|
||||
not_before, valid_at, not_after = self._validate_valid_at()
|
||||
if not (not_before <= valid_at <= not_after):
|
||||
messages.append(
|
||||
'Certificate is not valid for the specified date (%s) - not_before: %s - not_after: %s' %
|
||||
(self.valid_at, not_before, not_after)
|
||||
)
|
||||
|
||||
if self.invalid_at is not None:
|
||||
not_before, invalid_at, not_after = self._validate_invalid_at()
|
||||
if not_before <= invalid_at <= not_after:
|
||||
messages.append(
|
||||
'Certificate is not invalid for the specified date (%s) - not_before: %s - not_after: %s' %
|
||||
(self.invalid_at, not_before, not_after)
|
||||
)
|
||||
|
||||
if self.valid_in is not None:
|
||||
not_before, valid_in, not_after = self._validate_valid_in()
|
||||
if not not_before <= valid_in <= not_after:
|
||||
messages.append(
|
||||
'Certificate is not valid in %s from now (that would be %s) - not_before: %s - not_after: %s' %
|
||||
(self.valid_in, valid_in, not_before, not_after)
|
||||
)
|
||||
return messages
|
||||
|
||||
def needs_regeneration(self):
|
||||
self._ensure_existing_certificate_loaded()
|
||||
if self.existing_certificate is None:
|
||||
self.messages = ['Certificate not provided']
|
||||
else:
|
||||
self.messages = self.assertonly()
|
||||
|
||||
return len(self.messages) != 0
|
||||
|
||||
def generate_certificate(self):
|
||||
self.module.fail_json(msg=' | '.join(self.messages))
|
||||
|
||||
def get_certificate_data(self):
|
||||
return self.existing_certificate_bytes
|
||||
|
||||
|
||||
class AssertOnlyCertificateBackendCryptography(AssertOnlyCertificateBackend):
|
||||
"""Validate the supplied cert, using the cryptography backend"""
|
||||
def __init__(self, module):
|
||||
super(AssertOnlyCertificateBackendCryptography, self).__init__(module, 'cryptography')
|
||||
|
||||
def _validate_privatekey(self):
|
||||
return cryptography_compare_public_keys(self.existing_certificate.public_key(), self.privatekey.public_key())
|
||||
|
||||
def _validate_csr_signature(self):
|
||||
if not self.csr.is_signature_valid:
|
||||
return False
|
||||
return cryptography_compare_public_keys(self.csr.public_key(), self.existing_certificate.public_key())
|
||||
|
||||
def _validate_csr_subject(self):
|
||||
return self.csr.subject == self.existing_certificate.subject
|
||||
|
||||
def _validate_csr_extensions(self):
|
||||
cert_exts = self.existing_certificate.extensions
|
||||
csr_exts = self.csr.extensions
|
||||
if len(cert_exts) != len(csr_exts):
|
||||
return False
|
||||
for cert_ext in cert_exts:
|
||||
try:
|
||||
csr_ext = csr_exts.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 _validate_signature_algorithms(self):
|
||||
if self.existing_certificate.signature_algorithm_oid._name not in self.signature_algorithms:
|
||||
return self.existing_certificate.signature_algorithm_oid._name
|
||||
|
||||
def _validate_subject(self):
|
||||
expected_subject = Name([NameAttribute(oid=cryptography_name_to_oid(sub[0]), value=to_text(sub[1]))
|
||||
for sub in self.subject])
|
||||
cert_subject = self.existing_certificate.subject
|
||||
if not compare_sets(expected_subject, cert_subject, self.subject_strict):
|
||||
return expected_subject, cert_subject
|
||||
|
||||
def _validate_issuer(self):
|
||||
expected_issuer = Name([NameAttribute(oid=cryptography_name_to_oid(iss[0]), value=to_text(iss[1]))
|
||||
for iss in self.issuer])
|
||||
cert_issuer = self.existing_certificate.issuer
|
||||
if not compare_sets(expected_issuer, cert_issuer, self.issuer_strict):
|
||||
return self.issuer, cert_issuer
|
||||
|
||||
def _validate_has_expired(self):
|
||||
cert_not_after = self.existing_certificate.not_valid_after
|
||||
cert_expired = cert_not_after < datetime.datetime.utcnow()
|
||||
return cert_expired
|
||||
|
||||
def _validate_version(self):
|
||||
if self.existing_certificate.version == x509.Version.v1:
|
||||
return 1
|
||||
if self.existing_certificate.version == x509.Version.v3:
|
||||
return 3
|
||||
return "unknown"
|
||||
|
||||
def _validate_key_usage(self):
|
||||
try:
|
||||
current_key_usage = self.existing_certificate.extensions.get_extension_for_class(x509.KeyUsage).value
|
||||
test_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 test_key_usage['key_agreement']:
|
||||
test_key_usage.update(dict(
|
||||
encipher_only=current_key_usage.encipher_only,
|
||||
decipher_only=current_key_usage.decipher_only
|
||||
))
|
||||
|
||||
key_usages = cryptography_parse_key_usage_params(self.key_usage)
|
||||
if not compare_dicts(key_usages, test_key_usage, self.key_usage_strict):
|
||||
return self.key_usage, [k for k, v in test_key_usage.items() if v is True]
|
||||
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.key_usage:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_extended_key_usage(self):
|
||||
try:
|
||||
current_ext_keyusage = self.existing_certificate.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value
|
||||
usages = [cryptography_name_to_oid(usage) for usage in self.extended_key_usage]
|
||||
expected_ext_keyusage = x509.ExtendedKeyUsage(usages)
|
||||
if not compare_sets(expected_ext_keyusage, current_ext_keyusage, self.extended_key_usage_strict):
|
||||
return [eku.value for eku in expected_ext_keyusage], [eku.value for eku in current_ext_keyusage]
|
||||
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.extended_key_usage:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_subject_alt_name(self):
|
||||
try:
|
||||
current_san = self.existing_certificate.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
|
||||
expected_san = [cryptography_get_name(san) for san in self.subject_alt_name]
|
||||
if not compare_sets(expected_san, current_san, self.subject_alt_name_strict):
|
||||
return self.subject_alt_name, current_san
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.subject_alt_name:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_not_before(self):
|
||||
return self.existing_certificate.not_valid_before
|
||||
|
||||
def _validate_not_after(self):
|
||||
return self.existing_certificate.not_valid_after
|
||||
|
||||
def _validate_valid_at(self):
|
||||
rt = get_relative_time_option(self.valid_at, 'valid_at', backend=self.backend)
|
||||
return self.existing_certificate.not_valid_before, rt, self.existing_certificate.not_valid_after
|
||||
|
||||
def _validate_invalid_at(self):
|
||||
rt = get_relative_time_option(self.invalid_at, 'invalid_at', backend=self.backend)
|
||||
return self.existing_certificate.not_valid_before, rt, self.existing_certificate.not_valid_after
|
||||
|
||||
def _validate_valid_in(self):
|
||||
valid_in_date = get_relative_time_option(self.valid_in, "valid_in", backend=self.backend)
|
||||
return self.existing_certificate.not_valid_before, valid_in_date, self.existing_certificate.not_valid_after
|
||||
|
||||
|
||||
class AssertOnlyCertificateBackendPyOpenSSL(AssertOnlyCertificateBackend):
|
||||
"""validate the supplied certificate."""
|
||||
|
||||
def __init__(self, module):
|
||||
super(AssertOnlyCertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
# Ensure inputs are properly sanitized before comparison.
|
||||
for param in ['signature_algorithms', 'key_usage', 'extended_key_usage',
|
||||
'subject_alt_name', 'subject', 'issuer', 'not_before',
|
||||
'not_after', 'valid_at', 'invalid_at']:
|
||||
attr = getattr(self, param)
|
||||
if isinstance(attr, list) and attr:
|
||||
if isinstance(attr[0], str):
|
||||
setattr(self, param, [to_bytes(item) for item in attr])
|
||||
elif isinstance(attr[0], tuple):
|
||||
setattr(self, param, [(to_bytes(item[0]), to_bytes(item[1])) for item in attr])
|
||||
elif isinstance(attr, tuple):
|
||||
setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items()))
|
||||
elif isinstance(attr, dict):
|
||||
setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items()))
|
||||
elif isinstance(attr, str):
|
||||
setattr(self, param, to_bytes(attr))
|
||||
|
||||
def _validate_privatekey(self):
|
||||
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD)
|
||||
ctx.use_privatekey(self.privatekey)
|
||||
ctx.use_certificate(self.existing_certificate)
|
||||
try:
|
||||
ctx.check_privatekey()
|
||||
return True
|
||||
except OpenSSL.SSL.Error:
|
||||
return False
|
||||
|
||||
def _validate_csr_signature(self):
|
||||
try:
|
||||
self.csr.verify(self.existing_certificate.get_pubkey())
|
||||
except OpenSSL.crypto.Error:
|
||||
return False
|
||||
|
||||
def _validate_csr_subject(self):
|
||||
if self.csr.get_subject() != self.existing_certificate.get_subject():
|
||||
return False
|
||||
|
||||
def _validate_csr_extensions(self):
|
||||
csr_extensions = self.csr.get_extensions()
|
||||
cert_extension_count = self.existing_certificate.get_extension_count()
|
||||
if len(csr_extensions) != cert_extension_count:
|
||||
return False
|
||||
for extension_number in range(0, cert_extension_count):
|
||||
cert_extension = self.existing_certificate.get_extension(extension_number)
|
||||
csr_extension = filter(lambda extension: extension.get_short_name() == cert_extension.get_short_name(), csr_extensions)
|
||||
if cert_extension.get_data() != list(csr_extension)[0].get_data():
|
||||
return False
|
||||
return True
|
||||
|
||||
def _validate_signature_algorithms(self):
|
||||
if self.existing_certificate.get_signature_algorithm() not in self.signature_algorithms:
|
||||
return self.existing_certificate.get_signature_algorithm()
|
||||
|
||||
def _validate_subject(self):
|
||||
expected_subject = [(OpenSSL._util.lib.OBJ_txt2nid(sub[0]), sub[1]) for sub in self.subject]
|
||||
cert_subject = self.existing_certificate.get_subject().get_components()
|
||||
current_subject = [(OpenSSL._util.lib.OBJ_txt2nid(sub[0]), sub[1]) for sub in cert_subject]
|
||||
if not compare_sets(expected_subject, current_subject, self.subject_strict):
|
||||
return expected_subject, current_subject
|
||||
|
||||
def _validate_issuer(self):
|
||||
expected_issuer = [(OpenSSL._util.lib.OBJ_txt2nid(iss[0]), iss[1]) for iss in self.issuer]
|
||||
cert_issuer = self.existing_certificate.get_issuer().get_components()
|
||||
current_issuer = [(OpenSSL._util.lib.OBJ_txt2nid(iss[0]), iss[1]) for iss in cert_issuer]
|
||||
if not compare_sets(expected_issuer, current_issuer, self.issuer_strict):
|
||||
return self.issuer, cert_issuer
|
||||
|
||||
def _validate_has_expired(self):
|
||||
# The following 3 lines are the same as the current PyOpenSSL code for cert.has_expired().
|
||||
# Older version of PyOpenSSL have a buggy implementation,
|
||||
# to avoid issues with those we added the code from a more recent release here.
|
||||
|
||||
time_string = to_native(self.existing_certificate.get_notAfter())
|
||||
not_after = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||
cert_expired = not_after < datetime.datetime.utcnow()
|
||||
return cert_expired
|
||||
|
||||
def _validate_version(self):
|
||||
# Version numbers in certs are off by one:
|
||||
# v1: 0, v2: 1, v3: 2 ...
|
||||
return self.existing_certificate.get_version() + 1
|
||||
|
||||
def _validate_key_usage(self):
|
||||
found = False
|
||||
for extension_idx in range(0, self.existing_certificate.get_extension_count()):
|
||||
extension = self.existing_certificate.get_extension(extension_idx)
|
||||
if extension.get_short_name() == b'keyUsage':
|
||||
found = True
|
||||
expected_extension = crypto.X509Extension(b"keyUsage", False, b', '.join(self.key_usage))
|
||||
key_usage = [usage.strip() for usage in to_text(expected_extension, errors='surrogate_or_strict').split(',')]
|
||||
current_ku = [usage.strip() for usage in to_text(extension, errors='surrogate_or_strict').split(',')]
|
||||
if not compare_sets(key_usage, current_ku, self.key_usage_strict):
|
||||
return self.key_usage, str(extension).split(', ')
|
||||
if not found:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.key_usage:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_extended_key_usage(self):
|
||||
found = False
|
||||
for extension_idx in range(0, self.existing_certificate.get_extension_count()):
|
||||
extension = self.existing_certificate.get_extension(extension_idx)
|
||||
if extension.get_short_name() == b'extendedKeyUsage':
|
||||
found = True
|
||||
extKeyUsage = [OpenSSL._util.lib.OBJ_txt2nid(keyUsage) for keyUsage in self.extended_key_usage]
|
||||
current_xku = [OpenSSL._util.lib.OBJ_txt2nid(usage.strip()) for usage in
|
||||
to_bytes(extension, errors='surrogate_or_strict').split(b',')]
|
||||
if not compare_sets(extKeyUsage, current_xku, self.extended_key_usage_strict):
|
||||
return self.extended_key_usage, str(extension).split(', ')
|
||||
if not found:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.extended_key_usage:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_subject_alt_name(self):
|
||||
found = False
|
||||
for extension_idx in range(0, self.existing_certificate.get_extension_count()):
|
||||
extension = self.existing_certificate.get_extension(extension_idx)
|
||||
if extension.get_short_name() == b'subjectAltName':
|
||||
found = True
|
||||
l_altnames = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
|
||||
to_text(extension, errors='surrogate_or_strict').split(', ')]
|
||||
sans = [pyopenssl_normalize_name_attribute(to_text(san, errors='surrogate_or_strict')) for san in self.subject_alt_name]
|
||||
if not compare_sets(sans, l_altnames, self.subject_alt_name_strict):
|
||||
return self.subject_alt_name, l_altnames
|
||||
if not found:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.subject_alt_name:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_not_before(self):
|
||||
return self.existing_certificate.get_notBefore()
|
||||
|
||||
def _validate_not_after(self):
|
||||
return self.existing_certificate.get_notAfter()
|
||||
|
||||
def _validate_valid_at(self):
|
||||
rt = get_relative_time_option(self.valid_at, "valid_at", backend=self.backend)
|
||||
rt = to_bytes(rt, errors='surrogate_or_strict')
|
||||
return self.existing_certificate.get_notBefore(), rt, self.existing_certificate.get_notAfter()
|
||||
|
||||
def _validate_invalid_at(self):
|
||||
rt = get_relative_time_option(self.invalid_at, "invalid_at", backend=self.backend)
|
||||
rt = to_bytes(rt, errors='surrogate_or_strict')
|
||||
return self.existing_certificate.get_notBefore(), rt, self.existing_certificate.get_notAfter()
|
||||
|
||||
def _validate_valid_in(self):
|
||||
valid_in_asn1 = get_relative_time_option(self.valid_in, "valid_in", backend=self.backend)
|
||||
valid_in_date = to_bytes(valid_in_asn1, errors='surrogate_or_strict')
|
||||
return self.existing_certificate.get_notBefore(), valid_in_date, self.existing_certificate.get_notAfter()
|
||||
|
||||
|
||||
class AssertOnlyCertificateProvider(CertificateProvider):
|
||||
def validate_module_args(self, module):
|
||||
module.deprecate("The 'assertonly' provider is deprecated; please see the examples of "
|
||||
"the 'x509_certificate' module on how to replace it with other modules",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
def needs_version_two_certs(self, module):
|
||||
return False
|
||||
|
||||
def create_backend(self, module, backend):
|
||||
if backend == 'cryptography':
|
||||
return AssertOnlyCertificateBackendCryptography(module)
|
||||
if backend == 'pyopenssl':
|
||||
return AssertOnlyCertificateBackendPyOpenSSL(module)
|
||||
|
||||
|
||||
def add_assertonly_provider_to_argument_spec(argument_spec):
|
||||
argument_spec.argument_spec['provider']['choices'].append('assertonly')
|
||||
argument_spec.argument_spec.update(dict(
|
||||
signature_algorithms=dict(type='list', elements='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
subject=dict(type='dict', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
subject_strict=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
issuer=dict(type='dict', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
issuer_strict=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
has_expired=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
version=dict(type='int', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
key_usage=dict(type='list', elements='str', aliases=['keyUsage'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
key_usage_strict=dict(type='bool', default=False, aliases=['keyUsage_strict'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
extended_key_usage=dict(type='list', elements='str', aliases=['extendedKeyUsage'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
extended_key_usage_strict=dict(type='bool', default=False, aliases=['extendedKeyUsage_strict'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
subject_alt_name_strict=dict(type='bool', default=False, aliases=['subjectAltName_strict'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
not_before=dict(type='str', aliases=['notBefore'], removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
not_after=dict(type='str', aliases=['notAfter'], removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
valid_at=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
invalid_at=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
valid_in=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
))
|
||||
@@ -0,0 +1,225 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import datetime
|
||||
import time
|
||||
import os
|
||||
|
||||
from ansible.module_utils._text 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 == 'pyopenssl':
|
||||
csr_subject = self.csr.get_subject()
|
||||
csr_subject_components = csr_subject.get_components()
|
||||
for k, v in csr_subject_components:
|
||||
if k.upper() == 'O':
|
||||
# Entrust does not support multiple validated organizations in a single certificate
|
||||
if self.csr_org is not None:
|
||||
self.module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations "
|
||||
"found in Subject DN: '{0}'. ".format(csr_subject)))
|
||||
else:
|
||||
self.csr_org = v
|
||||
elif self.backend == 'cryptography':
|
||||
csr_subject_orgs = self.csr.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)
|
||||
if len(csr_subject_orgs) == 1:
|
||||
self.csr_org = csr_subject_orgs[0].value
|
||||
elif len(csr_subject_orgs) > 1:
|
||||
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 == 'pyopenssl':
|
||||
serial_number = "{0:X}".format(self.existing_certificate.get_serial_number())
|
||||
time_string = to_native(self.existing_certificate.get_notAfter())
|
||||
expiry = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||
elif 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']]
|
||||
)
|
||||
363
plugins/module_utils/crypto/module_backends/certificate_ownca.py
Normal file
363
plugins/module_utils/crypto/module_backends/certificate_ownca.py
Normal file
@@ -0,0 +1,363 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from random import randrange
|
||||
|
||||
from ansible.module_utils._text import to_bytes
|
||||
|
||||
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_key_needs_digest_for_signing,
|
||||
cryptography_serial_number_of_cert,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
CRYPTOGRAPHY_VERSION,
|
||||
CertificateError,
|
||||
CertificateBackend,
|
||||
CertificateProvider,
|
||||
)
|
||||
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
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 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():
|
||||
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())
|
||||
|
||||
self._ensure_existing_certificate_loaded()
|
||||
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 OwnCACertificateBackendPyOpenSSL(CertificateBackend):
|
||||
def __init__(self, module):
|
||||
super(OwnCACertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
self.notBefore = get_relative_time_option(self.module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend)
|
||||
self.notAfter = get_relative_time_option(self.module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend)
|
||||
self.digest = self.module.params['ownca_digest']
|
||||
self.version = self.module.params['ownca_version']
|
||||
self.serial_number = generate_serial_number()
|
||||
if self.module.params['ownca_create_subject_key_identifier'] != 'create_if_not_provided':
|
||||
self.module.fail_json(msg='ownca_create_subject_key_identifier cannot be used with the pyOpenSSL backend!')
|
||||
if self.module.params['ownca_create_authority_key_identifier']:
|
||||
self.module.warn('ownca_create_authority_key_identifier is ignored by the pyOpenSSL backend!')
|
||||
self.ca_cert_path = self.module.params['ownca_path']
|
||||
self.ca_cert_content = self.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 = self.module.params['ownca_privatekey_path']
|
||||
self.ca_privatekey_content = self.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 = self.module.params['ownca_privatekey_passphrase']
|
||||
|
||||
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,
|
||||
)
|
||||
try:
|
||||
self.ca_privatekey = load_privatekey(
|
||||
path=self.ca_privatekey_path,
|
||||
content=self.ca_privatekey_content,
|
||||
passphrase=self.ca_privatekey_passphrase
|
||||
)
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
self.module.fail_json(msg=str(exc))
|
||||
|
||||
def generate_certificate(self):
|
||||
"""(Re-)Generate certificate."""
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(self.serial_number)
|
||||
cert.set_notBefore(to_bytes(self.notBefore))
|
||||
cert.set_notAfter(to_bytes(self.notAfter))
|
||||
cert.set_subject(self.csr.get_subject())
|
||||
cert.set_issuer(self.ca_cert.get_subject())
|
||||
cert.set_version(self.version - 1)
|
||||
cert.set_pubkey(self.csr.get_pubkey())
|
||||
cert.add_extensions(self.csr.get_extensions())
|
||||
|
||||
cert.sign(self.ca_privatekey, self.digest)
|
||||
self.cert = cert
|
||||
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(OwnCACertificateBackendPyOpenSSL, 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,
|
||||
'notAfter': self.notAfter,
|
||||
'serial_number': self.serial_number,
|
||||
})
|
||||
else:
|
||||
if self.cert is None:
|
||||
self.cert = self.existing_certificate
|
||||
result.update({
|
||||
'notBefore': self.cert.get_notBefore(),
|
||||
'notAfter': self.cert.get_notAfter(),
|
||||
'serial_number': self.cert.get_serial_number(),
|
||||
})
|
||||
|
||||
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)
|
||||
if backend == 'pyopenssl':
|
||||
return OwnCACertificateBackendPyOpenSSL(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'],
|
||||
])
|
||||
@@ -0,0 +1,263 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from random import randrange
|
||||
|
||||
from ansible.module_utils._text import to_bytes
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
CertificateError,
|
||||
CertificateBackend,
|
||||
CertificateProvider,
|
||||
)
|
||||
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
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 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 SelfSignedCertificateBackendPyOpenSSL(CertificateBackend):
|
||||
def __init__(self, module):
|
||||
super(SelfSignedCertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
if module.params['selfsigned_create_subject_key_identifier'] != 'create_if_not_provided':
|
||||
module.fail_json(msg='selfsigned_create_subject_key_identifier cannot be used with the pyOpenSSL backend!')
|
||||
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 = module.params['selfsigned_digest']
|
||||
self.version = module.params['selfsigned_version']
|
||||
self.serial_number = generate_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._ensure_private_key_loaded()
|
||||
|
||||
self._ensure_csr_loaded()
|
||||
if self.csr is None:
|
||||
# Create empty CSR on the fly
|
||||
self.csr = crypto.X509Req()
|
||||
self.csr.set_pubkey(self.privatekey)
|
||||
self.csr.sign(self.privatekey, self.digest)
|
||||
|
||||
def generate_certificate(self):
|
||||
"""(Re-)Generate certificate."""
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(self.serial_number)
|
||||
cert.set_notBefore(to_bytes(self.notBefore))
|
||||
cert.set_notAfter(to_bytes(self.notAfter))
|
||||
cert.set_subject(self.csr.get_subject())
|
||||
cert.set_issuer(self.csr.get_subject())
|
||||
cert.set_version(self.version - 1)
|
||||
cert.set_pubkey(self.csr.get_pubkey())
|
||||
cert.add_extensions(self.csr.get_extensions())
|
||||
|
||||
cert.sign(self.privatekey, self.digest)
|
||||
self.cert = cert
|
||||
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(SelfSignedCertificateBackendPyOpenSSL, self).dump(include_certificate)
|
||||
|
||||
if self.module.check_mode:
|
||||
result.update({
|
||||
'notBefore': self.notBefore,
|
||||
'notAfter': self.notAfter,
|
||||
'serial_number': self.serial_number,
|
||||
})
|
||||
else:
|
||||
if self.cert is None:
|
||||
self.cert = self.existing_certificate
|
||||
result.update({
|
||||
'notBefore': self.cert.get_notBefore(),
|
||||
'notAfter': self.cert.get_notAfter(),
|
||||
'serial_number': self.cert.get_serial_number(),
|
||||
})
|
||||
|
||||
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)
|
||||
if backend == 'pyopenssl':
|
||||
return SelfSignedCertificateBackendPyOpenSSL(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']
|
||||
),
|
||||
))
|
||||
34
plugins/module_utils/crypto/module_backends/common.py
Normal file
34
plugins/module_utils/crypto/module_backends/common.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
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)
|
||||
842
plugins/module_utils/crypto/module_backends/csr.py
Normal file
842
plugins/module_utils/crypto/module_backends/csr.py
Normal file
@@ -0,0 +1,842 @@
|
||||
# -*- 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 COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import binascii
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
|
||||
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,
|
||||
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.pyopenssl_support import (
|
||||
pyopenssl_normalize_name_attribute,
|
||||
pyopenssl_parse_name_constraints,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
|
||||
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# OpenSSL 1.1.0 or newer
|
||||
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
|
||||
else:
|
||||
# OpenSSL 1.0.x or older
|
||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
||||
|
||||
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.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']),
|
||||
]
|
||||
|
||||
if module.params['subject']:
|
||||
self.subject = self.subject + parse_name_field(module.params['subject'])
|
||||
self.subject = [(entry[0], entry[1]) for entry in self.subject if entry[1]]
|
||||
|
||||
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
|
||||
|
||||
@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
|
||||
|
||||
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,
|
||||
}
|
||||
if include_csr:
|
||||
# Get hold of CSR bytes
|
||||
csr_bytes = self.existing_csr_bytes
|
||||
if self.csr is not None:
|
||||
csr_bytes = self.get_csr_data()
|
||||
# Store result
|
||||
result['csr'] = csr_bytes.decode('utf-8') if csr_bytes else None
|
||||
return result
|
||||
|
||||
|
||||
# Implementation with using pyOpenSSL
|
||||
class CertificateSigningRequestPyOpenSSLBackend(CertificateSigningRequestBackend):
|
||||
def __init__(self, module):
|
||||
for o in ('create_subject_key_identifier', ):
|
||||
if module.params[o]:
|
||||
module.fail_json(msg='You cannot use {0} with the pyOpenSSL backend!'.format(o))
|
||||
for o in ('subject_key_identifier', 'authority_key_identifier', 'authority_cert_issuer', 'authority_cert_serial_number', 'crl_distribution_points'):
|
||||
if module.params[o] is not None:
|
||||
module.fail_json(msg='You cannot use {0} with the pyOpenSSL backend!'.format(o))
|
||||
super(CertificateSigningRequestPyOpenSSLBackend, self).__init__(module, 'pyopenssl')
|
||||
|
||||
def generate_csr(self):
|
||||
"""(Re-)Generate CSR."""
|
||||
self._ensure_private_key_loaded()
|
||||
|
||||
req = crypto.X509Req()
|
||||
req.set_version(self.version - 1)
|
||||
subject = req.get_subject()
|
||||
for entry in self.subject:
|
||||
if entry[1] is not None:
|
||||
# Workaround for https://github.com/pyca/pyopenssl/issues/165
|
||||
nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(entry[0]))
|
||||
if nid == 0:
|
||||
raise CertificateSigningRequestError('Unknown subject field identifier "{0}"'.format(entry[0]))
|
||||
res = OpenSSL._util.lib.X509_NAME_add_entry_by_NID(subject._name, nid, OpenSSL._util.lib.MBSTRING_UTF8, to_bytes(entry[1]), -1, -1, 0)
|
||||
if res == 0:
|
||||
raise CertificateSigningRequestError('Invalid value for subject field identifier "{0}": {1}'.format(entry[0], entry[1]))
|
||||
|
||||
extensions = []
|
||||
if self.subjectAltName:
|
||||
altnames = ', '.join(self.subjectAltName)
|
||||
try:
|
||||
extensions.append(crypto.X509Extension(b"subjectAltName", self.subjectAltName_critical, altnames.encode('ascii')))
|
||||
except OpenSSL.crypto.Error as e:
|
||||
raise CertificateSigningRequestError(
|
||||
'Error while parsing Subject Alternative Names {0} (check for missing type prefix, such as "DNS:"!): {1}'.format(
|
||||
', '.join(["{0}".format(san) for san in self.subjectAltName]), str(e)
|
||||
)
|
||||
)
|
||||
|
||||
if self.keyUsage:
|
||||
usages = ', '.join(self.keyUsage)
|
||||
extensions.append(crypto.X509Extension(b"keyUsage", self.keyUsage_critical, usages.encode('ascii')))
|
||||
|
||||
if self.extendedKeyUsage:
|
||||
usages = ', '.join(self.extendedKeyUsage)
|
||||
extensions.append(crypto.X509Extension(b"extendedKeyUsage", self.extendedKeyUsage_critical, usages.encode('ascii')))
|
||||
|
||||
if self.basicConstraints:
|
||||
usages = ', '.join(self.basicConstraints)
|
||||
extensions.append(crypto.X509Extension(b"basicConstraints", self.basicConstraints_critical, usages.encode('ascii')))
|
||||
|
||||
if self.name_constraints_permitted or self.name_constraints_excluded:
|
||||
usages = ', '.join(
|
||||
['permitted;{0}'.format(name) for name in self.name_constraints_permitted] +
|
||||
['excluded;{0}'.format(name) for name in self.name_constraints_excluded]
|
||||
)
|
||||
extensions.append(crypto.X509Extension(b"nameConstraints", self.name_constraints_critical, usages.encode('ascii')))
|
||||
|
||||
if self.ocspMustStaple:
|
||||
extensions.append(crypto.X509Extension(OPENSSL_MUST_STAPLE_NAME, self.ocspMustStaple_critical, OPENSSL_MUST_STAPLE_VALUE))
|
||||
|
||||
if extensions:
|
||||
req.add_extensions(extensions)
|
||||
|
||||
req.set_pubkey(self.privatekey)
|
||||
req.sign(self.privatekey, self.digest)
|
||||
self.csr = req
|
||||
|
||||
def get_csr_data(self):
|
||||
"""Return bytes for self.csr."""
|
||||
return crypto.dump_certificate_request(crypto.FILETYPE_PEM, self.csr)
|
||||
|
||||
def _check_csr(self):
|
||||
def _check_subject(csr):
|
||||
subject = [(OpenSSL._util.lib.OBJ_txt2nid(to_bytes(sub[0])), to_bytes(sub[1])) for sub in self.subject]
|
||||
current_subject = [(OpenSSL._util.lib.OBJ_txt2nid(to_bytes(sub[0])), to_bytes(sub[1])) for sub in csr.get_subject().get_components()]
|
||||
if not set(subject) == set(current_subject):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _check_subjectAltName(extensions):
|
||||
altnames_ext = next((ext for ext in extensions if ext.get_short_name() == b'subjectAltName'), '')
|
||||
altnames = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
|
||||
to_text(altnames_ext, errors='surrogate_or_strict').split(',') if altname.strip()]
|
||||
if self.subjectAltName:
|
||||
if (set(altnames) != set([pyopenssl_normalize_name_attribute(to_text(name)) for name in self.subjectAltName]) or
|
||||
altnames_ext.get_critical() != self.subjectAltName_critical):
|
||||
return False
|
||||
else:
|
||||
if altnames:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _check_keyUsage_(extensions, extName, expected, critical):
|
||||
usages_ext = [ext for ext in extensions if ext.get_short_name() == extName]
|
||||
if (not usages_ext and expected) or (usages_ext and not expected):
|
||||
return False
|
||||
elif not usages_ext and not expected:
|
||||
return True
|
||||
else:
|
||||
current = [OpenSSL._util.lib.OBJ_txt2nid(to_bytes(usage.strip())) for usage in str(usages_ext[0]).split(',')]
|
||||
expected = [OpenSSL._util.lib.OBJ_txt2nid(to_bytes(usage)) for usage in expected]
|
||||
return set(current) == set(expected) and usages_ext[0].get_critical() == critical
|
||||
|
||||
def _check_keyUsage(extensions):
|
||||
usages_ext = [ext for ext in extensions if ext.get_short_name() == b'keyUsage']
|
||||
if (not usages_ext and self.keyUsage) or (usages_ext and not self.keyUsage):
|
||||
return False
|
||||
elif not usages_ext and not self.keyUsage:
|
||||
return True
|
||||
else:
|
||||
# OpenSSL._util.lib.OBJ_txt2nid() always returns 0 for all keyUsage values
|
||||
# (since keyUsage has a fixed bitfield for these values and is not extensible).
|
||||
# Therefore, we create an extension for the wanted values, and compare the
|
||||
# data of the extensions (which is the serialized bitfield).
|
||||
expected_ext = crypto.X509Extension(b"keyUsage", False, ', '.join(self.keyUsage).encode('ascii'))
|
||||
return usages_ext[0].get_data() == expected_ext.get_data() and usages_ext[0].get_critical() == self.keyUsage_critical
|
||||
|
||||
def _check_extenededKeyUsage(extensions):
|
||||
return _check_keyUsage_(extensions, b'extendedKeyUsage', self.extendedKeyUsage, self.extendedKeyUsage_critical)
|
||||
|
||||
def _check_basicConstraints(extensions):
|
||||
return _check_keyUsage_(extensions, b'basicConstraints', self.basicConstraints, self.basicConstraints_critical)
|
||||
|
||||
def _check_nameConstraints(extensions):
|
||||
nc_ext = next((ext for ext in extensions if ext.get_short_name() == b'nameConstraints'), '')
|
||||
permitted, excluded = pyopenssl_parse_name_constraints(nc_ext)
|
||||
if self.name_constraints_permitted or self.name_constraints_excluded:
|
||||
if set(permitted) != set([pyopenssl_normalize_name_attribute(to_text(name)) for name in self.name_constraints_permitted]):
|
||||
return False
|
||||
if set(excluded) != set([pyopenssl_normalize_name_attribute(to_text(name)) for name in self.name_constraints_excluded]):
|
||||
return False
|
||||
if nc_ext.get_critical() != self.name_constraints_critical:
|
||||
return False
|
||||
else:
|
||||
if permitted or excluded:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _check_ocspMustStaple(extensions):
|
||||
oms_ext = [ext for ext in extensions if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE]
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
|
||||
# Older versions of libssl don't know about OCSP Must Staple
|
||||
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
|
||||
if self.ocspMustStaple:
|
||||
return len(oms_ext) > 0 and oms_ext[0].get_critical() == self.ocspMustStaple_critical
|
||||
else:
|
||||
return len(oms_ext) == 0
|
||||
|
||||
def _check_extensions(csr):
|
||||
extensions = csr.get_extensions()
|
||||
return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and
|
||||
_check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and
|
||||
_check_ocspMustStaple(extensions) and _check_nameConstraints(extensions))
|
||||
|
||||
def _check_signature(csr):
|
||||
try:
|
||||
return csr.verify(self.privatekey)
|
||||
except crypto.Error:
|
||||
return False
|
||||
|
||||
return _check_subject(self.existing_csr) and _check_extensions(self.existing_csr) and _check_signature(self.existing_csr)
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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 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],
|
||||
[cryptography_get_name(name, 'name constraints excluded') for name in self.name_constraints_excluded],
|
||||
), 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]), entry[1]) for entry in self.subject]
|
||||
current_subject = [(sub.oid, sub.value) for sub in csr.subject]
|
||||
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 = [str(altname) for altname in current_altnames_ext.value] if current_altnames_ext else []
|
||||
altnames = [str(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:
|
||||
if bc_ext.critical != self.basicConstraints_critical:
|
||||
return False
|
||||
return True
|
||||
|
||||
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 = [str(altname) for altname in current_nc_ext.value.permitted_subtrees] if current_nc_ext else []
|
||||
current_nc_excl = [str(altname) for altname in current_nc_ext.value.excluded_subtrees] if current_nc_ext else []
|
||||
nc_perm = [str(cryptography_get_name(altname, 'name constraints permitted')) for altname in self.name_constraints_permitted]
|
||||
nc_excl = [str(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 = [str(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 = [str(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 module.params['version'] != 1:
|
||||
module.deprecate('The version option will only support allowed values from community.crypto 2.0.0 on. '
|
||||
'Currently, only the value 1 is allowed by RFC 2986',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# First try cryptography, then pyOpenSSL
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
try:
|
||||
getattr(crypto.X509Req, 'get_extensions')
|
||||
except AttributeError:
|
||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
|
||||
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, CertificateSigningRequestPyOpenSSLBackend(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, 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),
|
||||
subject=dict(type='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')]
|
||||
),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
),
|
||||
required_together=[
|
||||
['authority_cert_issuer', 'authority_cert_serial_number'],
|
||||
],
|
||||
mutually_exclusive=[
|
||||
['privatekey_path', 'privatekey_content'],
|
||||
],
|
||||
required_one_of=[
|
||||
['privatekey_path', 'privatekey_content'],
|
||||
],
|
||||
)
|
||||
590
plugins/module_utils/crypto/module_backends/privatekey.py
Normal file
590
plugins/module_utils/crypto/module_backends/privatekey.py
Normal file
@@ -0,0 +1,590 @@
|
||||
# -*- 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 COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import base64
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils._text import to_bytes
|
||||
|
||||
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,
|
||||
OpenSSLBadPassphraseError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
load_privatekey,
|
||||
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.common import ArgumentSpec
|
||||
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.6'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
import cryptography.exceptions
|
||||
import cryptography.hazmat.backends
|
||||
import cryptography.hazmat.primitives.serialization
|
||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
||||
import cryptography.hazmat.primitives.asymmetric.dsa
|
||||
import cryptography.hazmat.primitives.asymmetric.ec
|
||||
import cryptography.hazmat.primitives.asymmetric.utils
|
||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
CRYPTOGRAPHY_FOUND = False
|
||||
else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
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
|
||||
|
||||
@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
|
||||
|
||||
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=yes`.')
|
||||
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=yes`.')
|
||||
# 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=yes`.'
|
||||
' 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
|
||||
if include_key:
|
||||
# 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()
|
||||
# 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
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Implementation with using pyOpenSSL
|
||||
class PrivateKeyPyOpenSSLBackend(PrivateKeyBackend):
|
||||
|
||||
def __init__(self, module):
|
||||
super(PrivateKeyPyOpenSSLBackend, self).__init__(module=module, backend='pyopenssl')
|
||||
|
||||
if self.type == 'RSA':
|
||||
self.openssl_type = crypto.TYPE_RSA
|
||||
elif self.type == 'DSA':
|
||||
self.openssl_type = crypto.TYPE_DSA
|
||||
else:
|
||||
self.module.fail_json(msg="PyOpenSSL backend only supports RSA and DSA keys.")
|
||||
|
||||
if self.format != 'auto_ignore':
|
||||
self.module.fail_json(msg="PyOpenSSL backend only supports auto_ignore format.")
|
||||
|
||||
def generate_private_key(self):
|
||||
"""(Re-)Generate private key."""
|
||||
self.private_key = crypto.PKey()
|
||||
try:
|
||||
self.private_key.generate_key(self.openssl_type, self.size)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise PrivateKeyError(exc)
|
||||
|
||||
def _ensure_existing_private_key_loaded(self):
|
||||
if self.existing_private_key is None and self.has_existing():
|
||||
try:
|
||||
self.existing_private_key = load_privatekey(
|
||||
None, self.passphrase, content=self.existing_private_key_bytes, backend=self.backend)
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
raise PrivateKeyError(exc)
|
||||
|
||||
def get_private_key_data(self):
|
||||
"""Return bytes for self.private_key"""
|
||||
if self.cipher and self.passphrase:
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.private_key,
|
||||
self.cipher, to_bytes(self.passphrase))
|
||||
else:
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.private_key)
|
||||
|
||||
def _check_passphrase(self):
|
||||
try:
|
||||
load_privatekey(None, self.passphrase, content=self.existing_private_key_bytes, backend=self.backend)
|
||||
return True
|
||||
except Exception as dummy:
|
||||
return False
|
||||
|
||||
def _check_size_and_type(self):
|
||||
return self.size == self.existing_private_key.bits() and self.openssl_type == self.existing_private_key.type()
|
||||
|
||||
def _check_format(self):
|
||||
# Not supported by this backend
|
||||
return True
|
||||
|
||||
|
||||
# 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)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# Decision
|
||||
if module.params['cipher'] and module.params['passphrase'] and module.params['cipher'] != 'auto':
|
||||
# First try pyOpenSSL, then cryptography
|
||||
if can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
elif can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
else:
|
||||
# First try cryptography, then pyOpenSSL
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, PrivateKeyPyOpenSSLBackend(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, 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', 'pyopenssl', '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']],
|
||||
],
|
||||
)
|
||||
36
plugins/module_utils/crypto/openssh.py
Normal file
36
plugins/module_utils/crypto/openssh.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2020, Doug Stanley <doug+ansible@technologixllc.com>
|
||||
#
|
||||
# Ansible 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.
|
||||
#
|
||||
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import re
|
||||
|
||||
|
||||
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
|
||||
@@ -54,3 +54,21 @@ def identify_private_key_format(content):
|
||||
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
|
||||
@@ -83,11 +83,9 @@ def get_fingerprint_of_bytes(source):
|
||||
return fingerprint
|
||||
|
||||
|
||||
def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl'):
|
||||
def get_fingerprint_of_privatekey(privatekey, backend='pyopenssl'):
|
||||
"""Generate the fingerprint of the public key. """
|
||||
|
||||
privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend)
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
try:
|
||||
publickey = crypto.dump_publickey(crypto.FILETYPE_ASN1, privatekey)
|
||||
@@ -112,6 +110,14 @@ def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl'):
|
||||
return get_fingerprint_of_bytes(publickey)
|
||||
|
||||
|
||||
def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl'):
|
||||
"""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)
|
||||
|
||||
|
||||
def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='pyopenssl'):
|
||||
"""Load the specified OpenSSL private key.
|
||||
|
||||
@@ -125,57 +131,57 @@ def load_privatekey(path, passphrase=None, check_passphrase=True, content=None,
|
||||
priv_key_detail = b_priv_key_fh.read()
|
||||
else:
|
||||
priv_key_detail = content
|
||||
|
||||
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 isn't 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.
|
||||
# Don't 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
|
||||
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 isn't 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.
|
||||
# Don't 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_certificate(path, content=None, backend='pyopenssl'):
|
||||
"""Load the specified certificate."""
|
||||
@@ -186,12 +192,15 @@ def load_certificate(path, content=None, backend='pyopenssl'):
|
||||
cert_content = cert_fh.read()
|
||||
else:
|
||||
cert_content = content
|
||||
if backend == 'pyopenssl':
|
||||
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_content)
|
||||
elif backend == 'cryptography':
|
||||
return x509.load_pem_x509_certificate(cert_content, cryptography_backend())
|
||||
except (IOError, OSError) as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
if backend == 'pyopenssl':
|
||||
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_content)
|
||||
elif backend == 'cryptography':
|
||||
try:
|
||||
return x509.load_pem_x509_certificate(cert_content, cryptography_backend())
|
||||
except ValueError as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
|
||||
|
||||
def load_certificate_request(path, content=None, backend='pyopenssl'):
|
||||
@@ -207,7 +216,10 @@ def load_certificate_request(path, content=None, backend='pyopenssl'):
|
||||
if backend == 'pyopenssl':
|
||||
return crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_content)
|
||||
elif backend == 'cryptography':
|
||||
return x509.load_pem_x509_csr(csr_content, cryptography_backend())
|
||||
try:
|
||||
return x509.load_pem_x509_csr(csr_content, cryptography_backend())
|
||||
except ValueError as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
|
||||
|
||||
def parse_name_field(input_dict):
|
||||
@@ -343,6 +355,10 @@ class OpenSSLObject(object):
|
||||
|
||||
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)
|
||||
|
||||
@@ -23,6 +23,7 @@ notes:
|
||||
accounts."
|
||||
- "This module was called C(acme_account_facts) before Ansible 2.8. The usage
|
||||
did not change."
|
||||
- Supports C(check_mode).
|
||||
options:
|
||||
retrieve_orders:
|
||||
description:
|
||||
@@ -55,9 +56,11 @@ EXAMPLES = '''
|
||||
that:
|
||||
- account_data.exists
|
||||
- name: Print account URI
|
||||
debug: var=account_data.account_uri
|
||||
ansible.builtin.debug:
|
||||
var: account_data.account_uri
|
||||
- name: Print account contacts
|
||||
debug: var=account_data.account.contact
|
||||
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:
|
||||
@@ -69,7 +72,8 @@ EXAMPLES = '''
|
||||
that:
|
||||
- account_data.exists
|
||||
- name: Print account contacts
|
||||
debug: var=account_data.account.contact
|
||||
ansible.builtin.debug:
|
||||
var: account_data.account.contact
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
|
||||
@@ -125,9 +125,23 @@ options:
|
||||
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
|
||||
required: true
|
||||
aliases: ['src']
|
||||
csr_content:
|
||||
description:
|
||||
- "Content of the CSR for the new certificate."
|
||||
- "Can be created with 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
|
||||
@@ -279,7 +293,7 @@ EXAMPLES = r'''
|
||||
- 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: /etc/pki/cert/csr/sample.com.csr
|
||||
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
|
||||
@@ -297,7 +311,7 @@ EXAMPLES = r'''
|
||||
# - copy:
|
||||
# dest: /var/www/{{ item.key }}/{{ item.value['http-01']['resource'] }}
|
||||
# content: "{{ item.value['http-01']['resource_value'] }}"
|
||||
# loop: "{{ sample_com_challenge.challenge_data | dictsort }}"
|
||||
# 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
|
||||
@@ -349,7 +363,7 @@ EXAMPLES = r'''
|
||||
# # 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 | dictsort }}"
|
||||
# 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
|
||||
@@ -512,6 +526,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
||||
cryptography_name_to_oid,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
split_pem_list,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme import (
|
||||
ModuleFailException,
|
||||
write_file,
|
||||
@@ -576,6 +594,7 @@ class ACMEClient(object):
|
||||
self.version = module.params['acme_version']
|
||||
self.challenge = module.params['challenge']
|
||||
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')
|
||||
@@ -613,7 +632,7 @@ class ACMEClient(object):
|
||||
# signed ACME request.
|
||||
pass
|
||||
|
||||
if not os.path.exists(self.csr):
|
||||
if self.csr is not None and not os.path.exists(self.csr):
|
||||
raise ModuleFailException("CSR %s not found" % (self.csr))
|
||||
|
||||
self._openssl_bin = module.get_bin_path('openssl', True)
|
||||
@@ -626,9 +645,9 @@ class ACMEClient(object):
|
||||
Parse the CSR and return the list of requested identifiers
|
||||
'''
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
return cryptography_get_csr_identifiers(self.module, self.csr)
|
||||
return cryptography_get_csr_identifiers(self.module, self.csr, self.csr_content)
|
||||
else:
|
||||
return openssl_get_csr_identifiers(self._openssl_bin, self.module, self.csr)
|
||||
return openssl_get_csr_identifiers(self._openssl_bin, self.module, self.csr, self.csr_content)
|
||||
|
||||
def _add_or_update_auth(self, identifier_type, identifier, auth):
|
||||
'''
|
||||
@@ -723,11 +742,13 @@ class ACMEClient(object):
|
||||
Validate the authorization provided in the auth dict. Returns True
|
||||
when the validation was successful and False when it was not.
|
||||
'''
|
||||
found_challenge = False
|
||||
for challenge in auth['challenges']:
|
||||
if self.challenge != challenge['type']:
|
||||
continue
|
||||
|
||||
uri = challenge['uri'] if self.version == 1 else challenge['url']
|
||||
found_challenge = True
|
||||
|
||||
challenge_response = {}
|
||||
if self.version == 1:
|
||||
@@ -740,6 +761,10 @@ class ACMEClient(object):
|
||||
if info['status'] not in [200, 202]:
|
||||
raise ModuleFailException("Error validating challenge: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||
|
||||
if not found_challenge:
|
||||
raise ModuleFailException("Found no challenge of type '{0}' for identifier {1}:{2}!".format(
|
||||
self.challenge, identifier_type, identifier))
|
||||
|
||||
status = ''
|
||||
|
||||
while status not in ['valid', 'invalid', 'revoked']:
|
||||
@@ -767,7 +792,7 @@ class ACMEClient(object):
|
||||
Return the certificate object as dict
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4
|
||||
'''
|
||||
csr = pem_to_der(self.csr)
|
||||
csr = pem_to_der(self.csr, self.csr_content)
|
||||
new_cert = {
|
||||
"csr": nopad_b64(csr),
|
||||
}
|
||||
@@ -808,17 +833,10 @@ class ACMEClient(object):
|
||||
chain = []
|
||||
|
||||
# Parse data
|
||||
lines = content.decode('utf-8').splitlines(True)
|
||||
current = []
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
current.append(line)
|
||||
if line.startswith('-----END CERTIFICATE-----'):
|
||||
if cert is None:
|
||||
cert = ''.join(current)
|
||||
else:
|
||||
chain.append(''.join(current))
|
||||
current = []
|
||||
certs = split_pem_list(content.decode('utf-8'), keep_inbetween=True)
|
||||
if certs:
|
||||
cert = certs[0]
|
||||
chain = certs[1:]
|
||||
|
||||
alternates = []
|
||||
|
||||
@@ -834,7 +852,7 @@ class ACMEClient(object):
|
||||
|
||||
process_links(info, f)
|
||||
|
||||
if cert is None or current:
|
||||
if cert is None:
|
||||
raise ModuleFailException("Failed to parse certificate chain download from {0}: {1} (headers: {2})".format(url, content, info))
|
||||
return {'cert': cert, 'chain': chain, 'alternates': alternates}
|
||||
|
||||
@@ -844,7 +862,7 @@ class ACMEClient(object):
|
||||
Return the certificate object as dict
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
|
||||
'''
|
||||
csr = pem_to_der(self.csr)
|
||||
csr = pem_to_der(self.csr, self.csr_content)
|
||||
new_cert = {
|
||||
"resource": "new-cert",
|
||||
"csr": nopad_b64(csr),
|
||||
@@ -930,7 +948,7 @@ class ACMEClient(object):
|
||||
self._new_order_v2()
|
||||
self.changed = True
|
||||
|
||||
def get_challenges_data(self):
|
||||
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.
|
||||
@@ -946,6 +964,9 @@ class ACMEClient(object):
|
||||
continue
|
||||
# We drop the type from the key to preserve backwards compatibility
|
||||
data[identifier] = self._get_challenge_data(auth, identifier_type, identifier)
|
||||
if first_step 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':
|
||||
@@ -1177,7 +1198,8 @@ def main():
|
||||
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']),
|
||||
csr=dict(type='path', required=True, aliases=['src']),
|
||||
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']),
|
||||
@@ -1199,9 +1221,11 @@ def main():
|
||||
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,
|
||||
)
|
||||
@@ -1229,7 +1253,8 @@ def main():
|
||||
client = ACMEClient(module)
|
||||
client.cert_days = cert_days
|
||||
other = dict()
|
||||
if client.is_first_step():
|
||||
is_first_step = client.is_first_step()
|
||||
if is_first_step:
|
||||
# First run: start challenges / start new order
|
||||
client.start_challenges()
|
||||
else:
|
||||
@@ -1242,7 +1267,7 @@ def main():
|
||||
finally:
|
||||
if module.params['deactivate_authzs']:
|
||||
client.deactivate_authzs()
|
||||
data, data_dns = client.get_challenges_data()
|
||||
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
|
||||
|
||||
@@ -25,6 +25,7 @@ notes:
|
||||
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."
|
||||
- Does not support C(check_mode).
|
||||
seealso:
|
||||
- name: The Let's Encrypt documentation
|
||||
description: Documentation for the Let's Encrypt Certification Authority.
|
||||
@@ -98,7 +99,7 @@ options:
|
||||
C(2) (cACompromise), C(3) (affiliationChanged), C(4) (superseded),
|
||||
C(5) (cessationOfOperation), C(6) (certificateHold),
|
||||
C(8) (removeFromCRL), C(9) (privilegeWithdrawn),
|
||||
C(10) (aACompromise)"
|
||||
C(10) (aACompromise)."
|
||||
type: int
|
||||
'''
|
||||
|
||||
@@ -114,8 +115,7 @@ EXAMPLES = '''
|
||||
certificate: /etc/httpd/ssl/sample.com.crt
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
'''
|
||||
RETURN = '''#'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ options:
|
||||
- "Content of the private key to use for this challenge certificate."
|
||||
- "Mutually exclusive with C(private_key_src)."
|
||||
type: str
|
||||
notes:
|
||||
- Does not support C(check_mode).
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
||||
@@ -124,6 +124,10 @@ import traceback
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
split_pem_list,
|
||||
)
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
@@ -194,27 +198,17 @@ def parse_PEM_list(module, text, source, fail_on_error=True):
|
||||
Parse concatenated PEM certificates. Return list of ``Certificate`` objects.
|
||||
'''
|
||||
result = []
|
||||
lines = text.splitlines(True)
|
||||
current = None
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
if line.startswith('-----BEGIN '):
|
||||
current = [line]
|
||||
elif current is not None:
|
||||
current.append(line)
|
||||
if line.startswith('-----END '):
|
||||
cert_pem = ''.join(current)
|
||||
current = None
|
||||
# Try to load PEM certificate
|
||||
try:
|
||||
cert = cryptography.x509.load_pem_x509_certificate(to_bytes(cert_pem), _cryptography_backend)
|
||||
result.append(Certificate(cert_pem, cert))
|
||||
except Exception as e:
|
||||
msg = 'Cannot parse certificate #{0} from {1}: {2}'.format(len(result) + 1, source, e)
|
||||
if fail_on_error:
|
||||
module.fail_json(msg=msg)
|
||||
else:
|
||||
module.warn(msg)
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ description:
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL
|
||||
backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
|
||||
- Support SNI only with python >= 2.7
|
||||
- Support SNI (L(Server Name Indication,https://en.wikipedia.org/wiki/Server_Name_Indication)) only with python >= 2.7.
|
||||
options:
|
||||
host:
|
||||
description:
|
||||
@@ -35,6 +35,12 @@ options:
|
||||
- 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.
|
||||
@@ -211,6 +217,7 @@ def main():
|
||||
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', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
),
|
||||
@@ -222,6 +229,7 @@ def main():
|
||||
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')
|
||||
|
||||
backend = module.params.get('select_crypto_backend')
|
||||
if backend == 'auto':
|
||||
@@ -297,7 +305,7 @@ def main():
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = CERT_NONE
|
||||
|
||||
cert = ctx.wrap_socket(sock, server_hostname=host).getpeercert(True)
|
||||
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:
|
||||
|
||||
@@ -81,9 +81,10 @@ options:
|
||||
Needs I(keyfile) or I(passphrase) option for authorization.
|
||||
LUKS container supports up to 8 keyslots. Parameter value
|
||||
is the path to the keyfile with the passphrase."
|
||||
- "NOTE that adding additional keys is *not idempotent*.
|
||||
A new keyslot will be used even if another keyslot already
|
||||
exists for this keyfile."
|
||||
- "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
|
||||
@@ -93,9 +94,9 @@ options:
|
||||
Needs I(keyfile) or I(passphrase) option for authorization. LUKS
|
||||
container supports up to 8 keyslots. Parameter value is a string
|
||||
with the new passphrase."
|
||||
- "NOTE that adding additional passphrase is *not idempotent*. A
|
||||
new keyslot will be used even if another keyslot already exists
|
||||
for this passphrase."
|
||||
- "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:
|
||||
@@ -103,7 +104,8 @@ options:
|
||||
- "Removes given key from the container on I(device). Does not
|
||||
remove the keyfile from filesystem.
|
||||
Parameter value is the path to the keyfile with the passphrase."
|
||||
- "NOTE that removing keys is *not idempotent*. Trying to remove
|
||||
- "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(yes)."
|
||||
@@ -114,9 +116,9 @@ options:
|
||||
description:
|
||||
- "Removes given passphrase from the container on I(device).
|
||||
Parameter value is a string with the passphrase to remove."
|
||||
- "NOTE that removing passphrases is I(not
|
||||
idempotent). Trying to remove a passphrase which no longer
|
||||
exists results in an error."
|
||||
- "NOTE that 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(yes)."
|
||||
@@ -168,6 +170,47 @@ options:
|
||||
- "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
|
||||
|
||||
requirements:
|
||||
- "cryptsetup"
|
||||
@@ -397,7 +440,19 @@ class CryptHandler(Handler):
|
||||
result = self._run_command([self._cryptsetup_bin, 'isLuks', device])
|
||||
return result[RETURN_CODE] == 0
|
||||
|
||||
def run_luks_create(self, device, keyfile, passphrase, keysize, cipher, hash_):
|
||||
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_, pbkdf):
|
||||
# create a new luks container; use batch mode to auto confirm
|
||||
luks_type = self._module.params['type']
|
||||
label = self._module.params['label']
|
||||
@@ -414,6 +469,8 @@ class CryptHandler(Handler):
|
||||
options.extend(['--cipher', cipher])
|
||||
if hash_ is not None:
|
||||
options.extend(['--hash', hash_])
|
||||
if pbkdf is not None:
|
||||
self._add_pbkdf_options(options, pbkdf)
|
||||
|
||||
args = [self._cryptsetup_bin, 'luksFormat']
|
||||
args.extend(options)
|
||||
@@ -454,13 +511,15 @@ class CryptHandler(Handler):
|
||||
% (device, result[STDERR]))
|
||||
|
||||
def run_luks_add_key(self, device, keyfile, passphrase, new_keyfile,
|
||||
new_passphrase):
|
||||
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])
|
||||
@@ -520,6 +579,28 @@ class CryptHandler(Handler):
|
||||
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):
|
||||
|
||||
@@ -627,7 +708,7 @@ class ConditionsHandler(Handler):
|
||||
self._module.fail_json(msg="Contradiction in setup: Asking to "
|
||||
"add a key to absent LUKS.")
|
||||
|
||||
return True
|
||||
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
|
||||
@@ -640,7 +721,7 @@ class ConditionsHandler(Handler):
|
||||
self._module.fail_json(msg="Contradiction in setup: Asking to "
|
||||
"remove a key from absent LUKS.")
|
||||
|
||||
return True
|
||||
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
|
||||
@@ -667,6 +748,17 @@ def run_module():
|
||||
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')],
|
||||
),
|
||||
)
|
||||
|
||||
mutually_exclusive = [
|
||||
@@ -714,6 +806,7 @@ def run_module():
|
||||
module.params['keysize'],
|
||||
module.params['cipher'],
|
||||
module.params['hash'],
|
||||
module.params['pbkdf'],
|
||||
)
|
||||
except ValueError as e:
|
||||
module.fail_json(msg="luks_device error: %s" % e)
|
||||
@@ -775,7 +868,8 @@ def run_module():
|
||||
module.params['keyfile'],
|
||||
module.params['passphrase'],
|
||||
module.params['new_keyfile'],
|
||||
module.params['new_passphrase'])
|
||||
module.params['new_passphrase'],
|
||||
module.params['pbkdf'])
|
||||
except ValueError as e:
|
||||
module.fail_json(msg="luks_device error: %s" % e)
|
||||
result['changed'] = True
|
||||
|
||||
@@ -53,6 +53,12 @@ options:
|
||||
- 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.
|
||||
@@ -94,7 +100,7 @@ options:
|
||||
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-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."
|
||||
@@ -216,12 +222,14 @@ import tempfile
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import MINYEAR, MAXYEAR
|
||||
from distutils.version import LooseVersion
|
||||
from shutil import copy2, rmtree
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import convert_relative_to_datetime
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.openssh import parse_openssh_version
|
||||
|
||||
|
||||
class CertificateError(Exception):
|
||||
@@ -235,6 +243,7 @@ class Certificate(object):
|
||||
self.force = module.params['force']
|
||||
self.type = module.params['type']
|
||||
self.signing_key = module.params['signing_key']
|
||||
self.use_agent = module.params['use_agent']
|
||||
self.pkcs11_provider = module.params['pkcs11_provider']
|
||||
self.public_key = module.params['public_key']
|
||||
self.path = module.params['path']
|
||||
@@ -273,6 +282,9 @@ class Certificate(object):
|
||||
if self.pkcs11_provider:
|
||||
args.extend(['-D', self.pkcs11_provider])
|
||||
|
||||
if self.use_agent:
|
||||
args.extend(['-U'])
|
||||
|
||||
validity = ""
|
||||
|
||||
if not (self.valid_from == "always" and self.valid_to == "forever"):
|
||||
@@ -547,6 +559,7 @@ def main():
|
||||
force=dict(type='bool', default=False),
|
||||
type=dict(type='str', choices=['host', 'user']),
|
||||
signing_key=dict(type='path'),
|
||||
use_agent=dict(type='bool', default=False),
|
||||
pkcs11_provider=dict(type='str'),
|
||||
public_key=dict(type='path'),
|
||||
path=dict(type='path', required=True),
|
||||
@@ -563,6 +576,21 @@ def main():
|
||||
required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])],
|
||||
)
|
||||
|
||||
if module.params['use_agent']:
|
||||
ssh = module.get_bin_path('ssh', True)
|
||||
proc = module.run_command([ssh, '-Vq'])
|
||||
ssh_version_string = proc[2].strip()
|
||||
ssh_version = parse_openssh_version(ssh_version_string)
|
||||
if ssh_version is None:
|
||||
module.fail_json(msg="Failed to parse ssh version")
|
||||
elif LooseVersion(ssh_version) < LooseVersion("7.6"):
|
||||
module.fail_json(
|
||||
msg=(
|
||||
"Signing with CA key in ssh agent requires ssh 7.6 or newer."
|
||||
" Your version is: %s"
|
||||
) % ssh_version_string
|
||||
)
|
||||
|
||||
def isBaseDir(path):
|
||||
base_dir = os.path.dirname(path) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
|
||||
@@ -12,7 +12,7 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: openssh_keypair
|
||||
author: "David Kainz (@lolcube)"
|
||||
short_description: Generate OpenSSH private and public keys.
|
||||
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)
|
||||
@@ -59,7 +59,7 @@ options:
|
||||
description:
|
||||
- Allows to configure in which situations the module is allowed to regenerate private keys.
|
||||
The module will always generate a new key if the destination file does not exist.
|
||||
- By default, the key will be regenerated when it doesn't match the module's options,
|
||||
- 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.
|
||||
@@ -91,6 +91,7 @@ options:
|
||||
notes:
|
||||
- In case the ssh key is broken or password protected, the module will fail.
|
||||
Set the I(force) option to C(yes) if you want to regenerate the keypair.
|
||||
- Supports C(check_mode).
|
||||
|
||||
extends_documentation_fragment: files
|
||||
'''
|
||||
@@ -118,17 +119,17 @@ EXAMPLES = '''
|
||||
|
||||
RETURN = '''
|
||||
size:
|
||||
description: Size (in bits) of the SSH private key
|
||||
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
|
||||
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
|
||||
description: Path to the generated SSH private key file.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: /tmp/id_ssh_rsa
|
||||
@@ -138,12 +139,12 @@ fingerprint:
|
||||
type: str
|
||||
sample: SHA256:r4YCZxihVjedH2OlfjVGI6Y5xAYtdCwk8VxKyzVyYfM
|
||||
public_key:
|
||||
description: The public key of the generated SSH private key
|
||||
description: The public key of the generated SSH private key.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: ssh-rsa AAAAB3Nza(...omitted...)veL4E3Xcw== test_key
|
||||
comment:
|
||||
description: The comment of the generated key
|
||||
description: The comment of the generated key.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: test@comment
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,7 @@ options:
|
||||
|
||||
seealso:
|
||||
- module: community.crypto.openssl_csr
|
||||
- module: community.crypto.openssl_csr_pipe
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
|
||||
175
plugins/modules/openssl_csr_pipe.py
Normal file
175
plugins/modules/openssl_csr_pipe.py
Normal file
@@ -0,0 +1,175 @@
|
||||
#!/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 COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
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 doesn't match the module's
|
||||
options, or if it seems to be corrupt."
|
||||
author:
|
||||
- Yanis Guenane (@Spredzy)
|
||||
- Felix Fontein (@felixfontein)
|
||||
options:
|
||||
content:
|
||||
description:
|
||||
- The existing CSR.
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.crypto.module_csr
|
||||
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
|
||||
- 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._text 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,
|
||||
)
|
||||
|
||||
backend = module.params['select_crypto_backend']
|
||||
backend, module_backend = select_backend(module, backend)
|
||||
try:
|
||||
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()
|
||||
@@ -15,7 +15,7 @@ short_description: Generate OpenSSL Diffie-Hellman Parameters
|
||||
description:
|
||||
- This module allows one to (re)generate OpenSSL DH-params.
|
||||
- This module uses file common arguments to specify generated file permissions.
|
||||
- "Please note that the module regenerates existing DH params if they don't
|
||||
- "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.
|
||||
@@ -71,6 +71,8 @@ options:
|
||||
type: bool
|
||||
default: no
|
||||
version_added: "1.0.0"
|
||||
notes:
|
||||
- Supports C(check_mode).
|
||||
extends_documentation_fragment:
|
||||
- files
|
||||
seealso:
|
||||
|
||||
@@ -27,10 +27,19 @@ options:
|
||||
choices: [ export, parse ]
|
||||
other_certificates:
|
||||
description:
|
||||
- List of other certificates to include. Pre 2.8 this parameter was called C(ca_certificates)
|
||||
- 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.
|
||||
@@ -115,6 +124,27 @@ EXAMPLES = r'''
|
||||
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_path: /opt/certs/keys/key.pem
|
||||
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
|
||||
@@ -201,6 +231,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
||||
load_certificate,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
split_pem_list,
|
||||
)
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
@@ -211,6 +245,15 @@ else:
|
||||
pyopenssl_found = True
|
||||
|
||||
|
||||
def load_certificate_set(filename):
|
||||
'''
|
||||
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) for cert in split_pem_list(data)]
|
||||
|
||||
|
||||
class PkcsError(OpenSSLObjectError):
|
||||
pass
|
||||
|
||||
@@ -226,6 +269,7 @@ class Pkcs(OpenSSLObject):
|
||||
)
|
||||
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']
|
||||
@@ -244,6 +288,17 @@ class Pkcs(OpenSSLObject):
|
||||
self.backup = module.params['backup']
|
||||
self.backup_file = None
|
||||
|
||||
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))
|
||||
else:
|
||||
self.other_certificates = [
|
||||
load_certificate(other_cert) for other_cert in self.other_certificates
|
||||
]
|
||||
|
||||
def check(self, module, perms_required=True):
|
||||
"""Ensure the resource is in its desired state."""
|
||||
|
||||
@@ -302,6 +357,17 @@ class Pkcs(OpenSSLObject):
|
||||
return False
|
||||
elif bool(self.pkcs12.get_friendlyname()) != 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 crypto.Error:
|
||||
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
|
||||
|
||||
@@ -329,9 +395,7 @@ class Pkcs(OpenSSLObject):
|
||||
self.pkcs12 = crypto.PKCS12()
|
||||
|
||||
if self.other_certificates:
|
||||
other_certs = [load_certificate(other_cert) for other_cert
|
||||
in self.other_certificates]
|
||||
self.pkcs12.set_ca_certificates(other_certs)
|
||||
self.pkcs12.set_ca_certificates(self.other_certificates)
|
||||
|
||||
if self.certificate_path:
|
||||
self.pkcs12.set_certificate(load_certificate(self.certificate_path))
|
||||
@@ -360,10 +424,12 @@ class Pkcs(OpenSSLObject):
|
||||
pkcs12_content = pkcs12_fh.read()
|
||||
p12 = crypto.load_pkcs12(pkcs12_content,
|
||||
self.passphrase)
|
||||
pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
||||
p12.get_privatekey())
|
||||
crt = crypto.dump_certificate(crypto.FILETYPE_PEM,
|
||||
p12.get_certificate())
|
||||
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,
|
||||
@@ -389,6 +455,7 @@ 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']),
|
||||
@@ -444,8 +511,9 @@ def main():
|
||||
changed = True
|
||||
else:
|
||||
pkey, cert, other_certs, friendly_name = pkcs12.parse()
|
||||
dump_content = '%s%s%s' % (to_native(pkey), to_native(cert), to_native(b''.join(other_certs)))
|
||||
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.set_fs_attributes_if_different(file_args, changed):
|
||||
|
||||
@@ -14,23 +14,6 @@ module: openssl_privatekey
|
||||
short_description: Generate OpenSSL private keys
|
||||
description:
|
||||
- This module allows one to (re)generate OpenSSL private keys.
|
||||
- One can generate L(RSA,https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29),
|
||||
L(DSA,https://en.wikipedia.org/wiki/Digital_Signature_Algorithm),
|
||||
L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) or
|
||||
L(EdDSA,https://en.wikipedia.org/wiki/EdDSA) private keys.
|
||||
- Keys are generated in PEM format.
|
||||
- "Please note that the module regenerates private keys if they don't match
|
||||
the module's options. In particular, if you provide another passphrase
|
||||
(or specify none), change the keysize, etc., the private key will be
|
||||
regenerated. If you are concerned that this could **overwrite your private key**,
|
||||
consider using the I(backup) option."
|
||||
- "The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the
|
||||
PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in Ansible 2.13."
|
||||
requirements:
|
||||
- Either cryptography >= 1.2.3 (older versions might work as well)
|
||||
- Or pyOpenSSL
|
||||
author:
|
||||
- Yanis Guenane (@Spredzy)
|
||||
- Felix Fontein (@felixfontein)
|
||||
@@ -41,48 +24,6 @@ options:
|
||||
type: str
|
||||
default: present
|
||||
choices: [ absent, present ]
|
||||
size:
|
||||
description:
|
||||
- Size (in bits) of the TLS/SSL key to generate.
|
||||
type: int
|
||||
default: 4096
|
||||
type:
|
||||
description:
|
||||
- The algorithm used to generate the TLS/SSL private key.
|
||||
- Note that C(ECC), C(X25519), C(X448), C(Ed25519) and C(Ed448) require the C(cryptography) backend.
|
||||
C(X25519) needs cryptography 2.5 or newer, while C(X448), C(Ed25519) and C(Ed448) require
|
||||
cryptography 2.6 or newer. For C(ECC), the minimal cryptography version required depends on the
|
||||
I(curve) option.
|
||||
type: str
|
||||
default: RSA
|
||||
choices: [ DSA, ECC, Ed25519, Ed448, RSA, X25519, X448 ]
|
||||
curve:
|
||||
description:
|
||||
- Note that not all curves are supported by all versions of C(cryptography).
|
||||
- For maximal interoperability, C(secp384r1) or C(secp256r1) should be used.
|
||||
- We use the curve names as defined in the
|
||||
L(IANA registry for TLS,https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8).
|
||||
type: str
|
||||
choices:
|
||||
- secp384r1
|
||||
- secp521r1
|
||||
- secp224r1
|
||||
- secp192r1
|
||||
- secp256r1
|
||||
- secp256k1
|
||||
- brainpoolP256r1
|
||||
- brainpoolP384r1
|
||||
- brainpoolP512r1
|
||||
- sect571k1
|
||||
- sect409k1
|
||||
- sect283k1
|
||||
- sect233k1
|
||||
- sect163k1
|
||||
- sect571r1
|
||||
- sect409r1
|
||||
- sect283r1
|
||||
- sect233r1
|
||||
- sect163r2
|
||||
force:
|
||||
description:
|
||||
- Should the key be regenerated even if it already exists.
|
||||
@@ -90,56 +31,13 @@ options:
|
||||
default: no
|
||||
path:
|
||||
description:
|
||||
- Name of the file in which the generated TLS/SSL private key will be written. It will have 0600 mode.
|
||||
- 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
|
||||
passphrase:
|
||||
description:
|
||||
- The passphrase for the private key.
|
||||
type: str
|
||||
cipher:
|
||||
description:
|
||||
- The cipher to encrypt the private key. (Valid values can be found by
|
||||
running `openssl list -cipher-algorithms` or `openssl list-cipher-algorithms`,
|
||||
depending on your OpenSSL version.)
|
||||
- When using the C(cryptography) backend, use C(auto).
|
||||
type: str
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
format:
|
||||
description:
|
||||
- Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format)
|
||||
is used for all keys which support it. Please note that not every key can be exported in any format.
|
||||
- The value C(auto) selects a fromat based on the key format. The value C(auto_ignore) does the same,
|
||||
but for existing private key files, it will not force a regenerate when its format is not the automatically
|
||||
selected one for generation.
|
||||
- Note that if the format for an existing private key mismatches, the key is *regenerated* by default.
|
||||
To change this behavior, use the I(format_mismatch) option.
|
||||
- The I(format) option is only supported by the C(cryptography) backend. The C(pyopenssl) backend will
|
||||
fail if a value different from C(auto_ignore) is used.
|
||||
type: str
|
||||
default: auto_ignore
|
||||
choices: [ pkcs1, pkcs8, raw, auto, auto_ignore ]
|
||||
version_added: '1.0.0'
|
||||
format_mismatch:
|
||||
description:
|
||||
- Determines behavior of the module if the format of a private key does not match the expected format, but all
|
||||
other parameters are as expected.
|
||||
- If set to C(regenerate) (default), generates a new private key.
|
||||
- If set to C(convert), the key will be converted to the new format instead.
|
||||
- Only supported by the C(cryptography) backend.
|
||||
type: str
|
||||
default: regenerate
|
||||
choices: [ regenerate, convert ]
|
||||
version_added: '1.0.0'
|
||||
backup:
|
||||
description:
|
||||
@@ -152,50 +50,19 @@ options:
|
||||
- If set to C(yes), will return the (current or generated) private key's content as I(privatekey).
|
||||
- Note that especially if the private key is not encrypted, you have to make sure that the returned
|
||||
value is treated appropriately and not accidentally written to logs etc.! Use with care!
|
||||
- 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: no
|
||||
version_added: '1.0.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 doesn't match the module's options,
|
||||
except when the key cannot be read or the passphrase does not match. Please note that
|
||||
this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence)
|
||||
is specified.
|
||||
- If set to C(never), the module will fail if the key cannot be read or the passphrase
|
||||
isn't matching, and will never regenerate an existing key.
|
||||
- If set to C(fail), the module will fail if the key does not correspond to the module's
|
||||
options.
|
||||
- If set to C(partial_idempotence), the key will be regenerated if it does not conform to
|
||||
the module's options. The key is B(not) regenerated if it cannot be read (broken file),
|
||||
the key is protected by an unknown passphrase, or when they key is not protected by a
|
||||
passphrase, but a passphrase is specified.
|
||||
- If set to C(full_idempotence), the key will be regenerated if it does not conform to the
|
||||
module's options. This is also the case if the key cannot be read (broken file), the key
|
||||
is protected by an unknown passphrase, or when they key is not protected by a passphrase,
|
||||
but a passphrase is specified. Make sure you have a B(backup) when using this option!
|
||||
- If set to C(always), the module will always regenerate the key. This is equivalent to
|
||||
setting I(force) to C(yes).
|
||||
- Note that if I(format_mismatch) is set to C(convert) and everything matches except the
|
||||
format, the key will always be converted, except if I(regenerate) is set to C(always).
|
||||
type: str
|
||||
choices:
|
||||
- never
|
||||
- fail
|
||||
- partial_idempotence
|
||||
- full_idempotence
|
||||
- always
|
||||
default: full_idempotence
|
||||
version_added: '1.0.0'
|
||||
extends_documentation_fragment:
|
||||
- files
|
||||
- ansible.builtin.files
|
||||
- community.crypto.module_privatekey
|
||||
seealso:
|
||||
- module: community.crypto.x509_certificate
|
||||
- module: community.crypto.openssl_csr
|
||||
- module: community.crypto.openssl_dhparam
|
||||
- module: community.crypto.openssl_pkcs12
|
||||
- module: community.crypto.openssl_publickey
|
||||
- module: community.crypto.openssl_privatekey_pipe
|
||||
- module: community.crypto.openssl_privatekey_info
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
@@ -273,15 +140,10 @@ privatekey:
|
||||
version_added: '1.0.0'
|
||||
'''
|
||||
|
||||
import abc
|
||||
import base64
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
load_file_if_exists,
|
||||
@@ -289,83 +151,32 @@ from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
)
|
||||
|
||||
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,
|
||||
OpenSSLBadPassphraseError,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
OpenSSLObject,
|
||||
load_privatekey,
|
||||
get_fingerprint,
|
||||
get_fingerprint_of_bytes,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.identify import (
|
||||
identify_private_key_format,
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey import (
|
||||
select_backend,
|
||||
get_privatekey_argument_spec,
|
||||
)
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.6'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
class PrivateKeyModule(OpenSSLObject):
|
||||
|
||||
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
|
||||
|
||||
|
||||
class PrivateKeyBase(OpenSSLObject):
|
||||
|
||||
def __init__(self, module):
|
||||
super(PrivateKeyBase, self).__init__(
|
||||
def __init__(self, module, module_backend):
|
||||
super(PrivateKeyModule, self).__init__(
|
||||
module.params['path'],
|
||||
module.params['state'],
|
||||
module.params['force'],
|
||||
module.check_mode
|
||||
module.check_mode,
|
||||
)
|
||||
self.size = module.params['size']
|
||||
self.passphrase = module.params['passphrase']
|
||||
self.cipher = module.params['cipher']
|
||||
self.privatekey = None
|
||||
self.fingerprint = {}
|
||||
self.format = module.params['format']
|
||||
self.format_mismatch = module.params['format_mismatch']
|
||||
self.privatekey_bytes = None
|
||||
self.module_backend = module_backend
|
||||
self.return_content = module.params['return_content']
|
||||
self.regenerate = module.params['regenerate']
|
||||
if self.regenerate == 'always':
|
||||
self.force = True
|
||||
if self.force:
|
||||
module_backend.regenerate = 'always'
|
||||
|
||||
self.backup = module.params['backup']
|
||||
self.backup_file = None
|
||||
@@ -373,510 +184,68 @@ class PrivateKeyBase(OpenSSLObject):
|
||||
if module.params['mode'] is None:
|
||||
module.params['mode'] = '0600'
|
||||
|
||||
@abc.abstractmethod
|
||||
def _generate_private_key(self):
|
||||
"""(Re-)Generate private key."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _ensure_private_key_loaded(self):
|
||||
"""Make sure that the private key has been loaded."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_private_key_data(self):
|
||||
"""Return bytes for self.privatekey"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_fingerprint(self):
|
||||
pass
|
||||
module_backend.set_existing(load_file_if_exists(self.path, module))
|
||||
|
||||
def generate(self, module):
|
||||
"""Generate a keypair."""
|
||||
|
||||
if not self.check(module, perms_required=False, ignore_conversion=True) or self.force:
|
||||
if self.module_backend.needs_regeneration():
|
||||
# Regenerate
|
||||
if self.backup:
|
||||
self.backup_file = module.backup_local(self.path)
|
||||
self._generate_private_key()
|
||||
privatekey_data = self._get_private_key_data()
|
||||
if self.return_content:
|
||||
self.privatekey_bytes = privatekey_data
|
||||
write_file(module, privatekey_data, 0o600)
|
||||
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 not self.check(module, perms_required=False, ignore_conversion=False):
|
||||
elif self.module_backend.needs_conversion():
|
||||
# Convert
|
||||
if self.backup:
|
||||
self.backup_file = module.backup_local(self.path)
|
||||
self._ensure_private_key_loaded()
|
||||
privatekey_data = self._get_private_key_data()
|
||||
if self.return_content:
|
||||
self.privatekey_bytes = privatekey_data
|
||||
write_file(module, privatekey_data, 0o600)
|
||||
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
|
||||
|
||||
self.fingerprint = self._get_fingerprint()
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if module.set_fs_attributes_if_different(file_args, False):
|
||||
self.changed = True
|
||||
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
|
||||
|
||||
def remove(self, module):
|
||||
if self.backup:
|
||||
self.module_backend.set_existing(None)
|
||||
if self.backup and not self.check_mode:
|
||||
self.backup_file = module.backup_local(self.path)
|
||||
super(PrivateKeyBase, self).remove(module)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _check_passphrase(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _check_size_and_type(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _check_format(self):
|
||||
pass
|
||||
|
||||
def check(self, module, perms_required=True, ignore_conversion=True):
|
||||
"""Ensure the resource is in its desired state."""
|
||||
|
||||
state_and_perms = super(PrivateKeyBase, self).check(module, perms_required=False)
|
||||
|
||||
if not state_and_perms:
|
||||
# key does not exist
|
||||
return False
|
||||
|
||||
if not self._check_passphrase():
|
||||
if self.regenerate in ('full_idempotence', 'always'):
|
||||
return False
|
||||
module.fail_json(msg='Unable to read the key. The key is protected with a another passphrase / no passphrase or broken.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `full_idempotence` or `always`, or with `force=yes`.')
|
||||
|
||||
if self.regenerate != 'never':
|
||||
if not self._check_size_and_type():
|
||||
if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'):
|
||||
return False
|
||||
module.fail_json(msg='Key has wrong type and/or size.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.')
|
||||
|
||||
if not self._check_format():
|
||||
# During conversion step, convert if format does not match and format_mismatch == 'convert'
|
||||
if not ignore_conversion and self.format_mismatch == 'convert':
|
||||
return False
|
||||
# During generation step, regenerate if format does not match and format_mismatch == 'regenerate'
|
||||
if ignore_conversion and self.format_mismatch == 'regenerate' and self.regenerate != 'never':
|
||||
if not ignore_conversion or self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'):
|
||||
return False
|
||||
module.fail_json(msg='Key has wrong format.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.'
|
||||
' To convert the key, set `format_mismatch` to `convert`.')
|
||||
|
||||
# check whether permissions are correct (in case that needs to be checked)
|
||||
return not perms_required or super(PrivateKeyBase, self).check(module, perms_required=perms_required)
|
||||
super(PrivateKeyModule, self).remove(module)
|
||||
|
||||
def dump(self):
|
||||
"""Serialize the object into a dictionary."""
|
||||
|
||||
result = {
|
||||
'size': self.size,
|
||||
'filename': self.path,
|
||||
'changed': self.changed,
|
||||
'fingerprint': self.fingerprint,
|
||||
}
|
||||
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
|
||||
if self.return_content:
|
||||
if self.privatekey_bytes is None:
|
||||
self.privatekey_bytes = load_file_if_exists(self.path, ignore_errors=True)
|
||||
if self.privatekey_bytes:
|
||||
if identify_private_key_format(self.privatekey_bytes) == 'raw':
|
||||
result['privatekey'] = base64.b64encode(self.privatekey_bytes)
|
||||
else:
|
||||
result['privatekey'] = self.privatekey_bytes.decode('utf-8')
|
||||
else:
|
||||
result['privatekey'] = None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Implementation with using pyOpenSSL
|
||||
class PrivateKeyPyOpenSSL(PrivateKeyBase):
|
||||
|
||||
def __init__(self, module):
|
||||
super(PrivateKeyPyOpenSSL, self).__init__(module)
|
||||
|
||||
if module.params['type'] == 'RSA':
|
||||
self.type = crypto.TYPE_RSA
|
||||
elif module.params['type'] == 'DSA':
|
||||
self.type = crypto.TYPE_DSA
|
||||
else:
|
||||
module.fail_json(msg="PyOpenSSL backend only supports RSA and DSA keys.")
|
||||
|
||||
if self.format != 'auto_ignore':
|
||||
module.fail_json(msg="PyOpenSSL backend only supports auto_ignore format.")
|
||||
|
||||
def _generate_private_key(self):
|
||||
"""(Re-)Generate private key."""
|
||||
self.privatekey = crypto.PKey()
|
||||
try:
|
||||
self.privatekey.generate_key(self.type, self.size)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise PrivateKeyError(exc)
|
||||
|
||||
def _ensure_private_key_loaded(self):
|
||||
"""Make sure that the private key has been loaded."""
|
||||
if self.privatekey is None:
|
||||
try:
|
||||
self.privatekey = privatekey = load_privatekey(self.path, self.passphrase)
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
raise PrivateKeyError(exc)
|
||||
|
||||
def _get_private_key_data(self):
|
||||
"""Return bytes for self.privatekey"""
|
||||
if self.cipher and self.passphrase:
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey,
|
||||
self.cipher, to_bytes(self.passphrase))
|
||||
else:
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey)
|
||||
|
||||
def _get_fingerprint(self):
|
||||
return get_fingerprint(self.path, self.passphrase)
|
||||
|
||||
def _check_passphrase(self):
|
||||
try:
|
||||
load_privatekey(self.path, self.passphrase)
|
||||
return True
|
||||
except Exception as dummy:
|
||||
return False
|
||||
|
||||
def _check_size_and_type(self):
|
||||
def _check_size(privatekey):
|
||||
return self.size == privatekey.bits()
|
||||
|
||||
def _check_type(privatekey):
|
||||
return self.type == privatekey.type()
|
||||
|
||||
self._ensure_private_key_loaded()
|
||||
return _check_size(self.privatekey) and _check_type(self.privatekey)
|
||||
|
||||
def _check_format(self):
|
||||
# Not supported by this backend
|
||||
return True
|
||||
|
||||
def dump(self):
|
||||
"""Serialize the object into a dictionary."""
|
||||
|
||||
result = super(PrivateKeyPyOpenSSL, self).dump()
|
||||
|
||||
if self.type == crypto.TYPE_RSA:
|
||||
result['type'] = 'RSA'
|
||||
else:
|
||||
result['type'] = 'DSA'
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Implementation with using cryptography
|
||||
class PrivateKeyCryptography(PrivateKeyBase):
|
||||
|
||||
def _get_ec_class(self, ectype):
|
||||
ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(ectype)
|
||||
if ecclass is None:
|
||||
self.module.fail_json(msg='Your cryptography version does not support {0}'.format(ectype))
|
||||
return ecclass
|
||||
|
||||
def _add_curve(self, name, ectype, deprecated=False):
|
||||
def create(size):
|
||||
ecclass = self._get_ec_class(ectype)
|
||||
return ecclass()
|
||||
|
||||
def verify(privatekey):
|
||||
ecclass = self._get_ec_class(ectype)
|
||||
return isinstance(privatekey.private_numbers().public_numbers.curve, ecclass)
|
||||
|
||||
self.curves[name] = {
|
||||
'create': create,
|
||||
'verify': verify,
|
||||
'deprecated': deprecated,
|
||||
}
|
||||
|
||||
def __init__(self, module):
|
||||
super(PrivateKeyCryptography, self).__init__(module)
|
||||
|
||||
self.curves = dict()
|
||||
self._add_curve('secp384r1', 'SECP384R1')
|
||||
self._add_curve('secp521r1', 'SECP521R1')
|
||||
self._add_curve('secp224r1', 'SECP224R1')
|
||||
self._add_curve('secp192r1', 'SECP192R1')
|
||||
self._add_curve('secp256r1', 'SECP256R1')
|
||||
self._add_curve('secp256k1', 'SECP256K1')
|
||||
self._add_curve('brainpoolP256r1', 'BrainpoolP256R1', deprecated=True)
|
||||
self._add_curve('brainpoolP384r1', 'BrainpoolP384R1', deprecated=True)
|
||||
self._add_curve('brainpoolP512r1', 'BrainpoolP512R1', deprecated=True)
|
||||
self._add_curve('sect571k1', 'SECT571K1', deprecated=True)
|
||||
self._add_curve('sect409k1', 'SECT409K1', deprecated=True)
|
||||
self._add_curve('sect283k1', 'SECT283K1', deprecated=True)
|
||||
self._add_curve('sect233k1', 'SECT233K1', deprecated=True)
|
||||
self._add_curve('sect163k1', 'SECT163K1', deprecated=True)
|
||||
self._add_curve('sect571r1', 'SECT571R1', deprecated=True)
|
||||
self._add_curve('sect409r1', 'SECT409R1', deprecated=True)
|
||||
self._add_curve('sect283r1', 'SECT283R1', deprecated=True)
|
||||
self._add_curve('sect233r1', 'SECT233R1', deprecated=True)
|
||||
self._add_curve('sect163r2', 'SECT163R2', deprecated=True)
|
||||
|
||||
self.module = module
|
||||
self.cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||
|
||||
self.type = module.params['type']
|
||||
self.curve = module.params['curve']
|
||||
if not CRYPTOGRAPHY_HAS_X25519 and self.type == 'X25519':
|
||||
self.module.fail_json(msg='Your cryptography version does not support X25519')
|
||||
if not CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519':
|
||||
self.module.fail_json(msg='Your cryptography version does not support X25519 serialization')
|
||||
if not CRYPTOGRAPHY_HAS_X448 and self.type == 'X448':
|
||||
self.module.fail_json(msg='Your cryptography version does not support X448')
|
||||
if not CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519':
|
||||
self.module.fail_json(msg='Your cryptography version does not support Ed25519')
|
||||
if not CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448':
|
||||
self.module.fail_json(msg='Your cryptography version does not support Ed448')
|
||||
|
||||
def _get_wanted_format(self):
|
||||
if self.format not in ('auto', 'auto_ignore'):
|
||||
return self.format
|
||||
if self.type in ('X25519', 'X448', 'Ed25519', 'Ed448'):
|
||||
return 'pkcs8'
|
||||
else:
|
||||
return 'pkcs1'
|
||||
|
||||
def _generate_private_key(self):
|
||||
"""(Re-)Generate private key."""
|
||||
try:
|
||||
if self.type == 'RSA':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key(
|
||||
public_exponent=65537, # OpenSSL always uses this
|
||||
key_size=self.size,
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
if self.type == 'DSA':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key(
|
||||
key_size=self.size,
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
if CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate()
|
||||
if CRYPTOGRAPHY_HAS_X448 and self.type == 'X448':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate()
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate()
|
||||
if CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.generate()
|
||||
if self.type == 'ECC' and self.curve in self.curves:
|
||||
if self.curves[self.curve]['deprecated']:
|
||||
self.module.warn('Elliptic curves of type {0} should not be used for new keys!'.format(self.curve))
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(
|
||||
curve=self.curves[self.curve]['create'](self.size),
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
except cryptography.exceptions.UnsupportedAlgorithm as dummy:
|
||||
self.module.fail_json(msg='Cryptography backend does not support the algorithm required for {0}'.format(self.type))
|
||||
|
||||
def _ensure_private_key_loaded(self):
|
||||
"""Make sure that the private key has been loaded."""
|
||||
if self.privatekey is None:
|
||||
self.privatekey = self._load_privatekey()
|
||||
|
||||
def _get_private_key_data(self):
|
||||
"""Return bytes for self.privatekey"""
|
||||
# Select export format and encoding
|
||||
try:
|
||||
export_format = self._get_wanted_format()
|
||||
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM
|
||||
if export_format == 'pkcs1':
|
||||
# "TraditionalOpenSSL" format is PKCS1
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL
|
||||
elif export_format == 'pkcs8':
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
|
||||
elif export_format == 'raw':
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.Raw
|
||||
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.Raw
|
||||
except AttributeError:
|
||||
self.module.fail_json(msg='Cryptography backend does not support the selected output format "{0}"'.format(self.format))
|
||||
|
||||
# Select key encryption
|
||||
encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption()
|
||||
if self.cipher and self.passphrase:
|
||||
if self.cipher == 'auto':
|
||||
encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(to_bytes(self.passphrase))
|
||||
else:
|
||||
self.module.fail_json(msg='Cryptography backend can only use "auto" for cipher option.')
|
||||
|
||||
# Serialize key
|
||||
try:
|
||||
return self.privatekey.private_bytes(
|
||||
encoding=export_encoding,
|
||||
format=export_format,
|
||||
encryption_algorithm=encryption_algorithm
|
||||
)
|
||||
except ValueError as dummy:
|
||||
self.module.fail_json(
|
||||
msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(self.format)
|
||||
)
|
||||
except Exception as dummy:
|
||||
self.module.fail_json(
|
||||
msg='Error while serializing the private key in the required format "{0}"'.format(self.format),
|
||||
exception=traceback.format_exc()
|
||||
)
|
||||
|
||||
def _load_privatekey(self):
|
||||
try:
|
||||
# Read bytes
|
||||
with open(self.path, 'rb') as f:
|
||||
data = f.read()
|
||||
# Interpret bytes depending on format.
|
||||
format = identify_private_key_format(data)
|
||||
if format == 'raw':
|
||||
if len(data) == 56 and CRYPTOGRAPHY_HAS_X448:
|
||||
return cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data)
|
||||
if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448:
|
||||
return cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data)
|
||||
if len(data) == 32:
|
||||
if CRYPTOGRAPHY_HAS_X25519 and (self.type == 'X25519' or not CRYPTOGRAPHY_HAS_ED25519):
|
||||
return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and (self.type == 'Ed25519' or not CRYPTOGRAPHY_HAS_X25519):
|
||||
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
|
||||
if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519:
|
||||
try:
|
||||
return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
|
||||
except Exception:
|
||||
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
|
||||
raise PrivateKeyError('Cannot load raw key')
|
||||
else:
|
||||
return cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
||||
data,
|
||||
None if self.passphrase is None else to_bytes(self.passphrase),
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
except Exception as e:
|
||||
raise PrivateKeyError(e)
|
||||
|
||||
def _get_fingerprint(self):
|
||||
# Get bytes of public key
|
||||
private_key = self._load_privatekey()
|
||||
public_key = private_key.public_key()
|
||||
public_key_bytes = public_key.public_bytes(
|
||||
cryptography.hazmat.primitives.serialization.Encoding.DER,
|
||||
cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
# Get fingerprints of public_key_bytes
|
||||
return get_fingerprint_of_bytes(public_key_bytes)
|
||||
|
||||
def _check_passphrase(self):
|
||||
try:
|
||||
with open(self.path, 'rb') as f:
|
||||
data = f.read()
|
||||
format = identify_private_key_format(data)
|
||||
if format == 'raw':
|
||||
# Raw keys cannot be encrypted. To avoid incompatibilities, we try to
|
||||
# actually load the key (and return False when this fails).
|
||||
self._load_privatekey()
|
||||
# Loading the key succeeded. Only return True when no passphrase was
|
||||
# provided.
|
||||
return self.passphrase is None
|
||||
else:
|
||||
return cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
||||
data,
|
||||
None if self.passphrase is None else to_bytes(self.passphrase),
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
except Exception as dummy:
|
||||
return False
|
||||
|
||||
def _check_size_and_type(self):
|
||||
self._ensure_private_key_loaded()
|
||||
|
||||
if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
return self.type == 'RSA' and self.size == self.privatekey.key_size
|
||||
if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
||||
return self.type == 'DSA' and self.size == self.privatekey.key_size
|
||||
if CRYPTOGRAPHY_HAS_X25519 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey):
|
||||
return self.type == 'X25519'
|
||||
if CRYPTOGRAPHY_HAS_X448 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey):
|
||||
return self.type == 'X448'
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
||||
return self.type == 'Ed25519'
|
||||
if CRYPTOGRAPHY_HAS_ED448 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
||||
return self.type == 'Ed448'
|
||||
if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
if self.type != 'ECC':
|
||||
return False
|
||||
if self.curve not in self.curves:
|
||||
return False
|
||||
return self.curves[self.curve]['verify'](self.privatekey)
|
||||
|
||||
return False
|
||||
|
||||
def _check_format(self):
|
||||
if self.format == 'auto_ignore':
|
||||
return True
|
||||
try:
|
||||
with open(self.path, 'rb') as f:
|
||||
content = f.read()
|
||||
format = identify_private_key_format(content)
|
||||
return format == self._get_wanted_format()
|
||||
except Exception as dummy:
|
||||
return False
|
||||
|
||||
def dump(self):
|
||||
"""Serialize the object into a dictionary."""
|
||||
result = super(PrivateKeyCryptography, self).dump()
|
||||
result['type'] = self.type
|
||||
if self.type == 'ECC':
|
||||
result['curve'] = self.curve
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
size=dict(type='int', default=4096),
|
||||
type=dict(type='str', default='RSA', choices=[
|
||||
'DSA', 'ECC', 'Ed25519', 'Ed448', 'RSA', 'X25519', 'X448'
|
||||
]),
|
||||
curve=dict(type='str', choices=[
|
||||
'secp384r1', 'secp521r1', 'secp224r1', 'secp192r1', 'secp256r1',
|
||||
'secp256k1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1',
|
||||
'sect571k1', 'sect409k1', 'sect283k1', 'sect233k1', 'sect163k1',
|
||||
'sect571r1', 'sect409r1', 'sect283r1', 'sect233r1', 'sect163r2',
|
||||
]),
|
||||
force=dict(type='bool', default=False),
|
||||
path=dict(type='path', required=True),
|
||||
passphrase=dict(type='str', no_log=True),
|
||||
cipher=dict(type='str'),
|
||||
backup=dict(type='bool', default=False),
|
||||
format=dict(type='str', default='auto_ignore', choices=['pkcs1', 'pkcs8', 'raw', 'auto', 'auto_ignore']),
|
||||
format_mismatch=dict(type='str', default='regenerate', choices=['regenerate', 'convert']),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
return_content=dict(type='bool', default=False),
|
||||
regenerate=dict(
|
||||
type='str',
|
||||
default='full_idempotence',
|
||||
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
|
||||
),
|
||||
),
|
||||
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,
|
||||
required_together=[
|
||||
['cipher', 'passphrase']
|
||||
],
|
||||
required_if=[
|
||||
['type', 'ECC', ['curve']],
|
||||
],
|
||||
)
|
||||
|
||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
||||
@@ -886,61 +255,17 @@ def main():
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
|
||||
backend = module.params['select_crypto_backend']
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
backend, module_backend = select_backend(
|
||||
module=module,
|
||||
backend=module.params['select_crypto_backend'],
|
||||
)
|
||||
|
||||
# Decision
|
||||
if module.params['cipher'] and module.params['passphrase'] and module.params['cipher'] != 'auto':
|
||||
# First try pyOpenSSL, then cryptography
|
||||
if can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
elif can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
else:
|
||||
# First try cryptography, then pyOpenSSL
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
try:
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
private_key = PrivateKeyPyOpenSSL(module)
|
||||
elif backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
private_key = PrivateKeyCryptography(module)
|
||||
private_key = PrivateKeyModule(module, module_backend)
|
||||
|
||||
if private_key.state == 'present':
|
||||
if module.check_mode:
|
||||
result = private_key.dump()
|
||||
result['changed'] = private_key.force \
|
||||
or not private_key.check(module, ignore_conversion=True) \
|
||||
or not private_key.check(module, ignore_conversion=False)
|
||||
module.exit_json(**result)
|
||||
|
||||
private_key.generate(module)
|
||||
else:
|
||||
if module.check_mode:
|
||||
result = private_key.dump()
|
||||
result['changed'] = os.path.exists(module.params['path'])
|
||||
module.exit_json(**result)
|
||||
|
||||
private_key.remove(module)
|
||||
|
||||
result = private_key.dump()
|
||||
|
||||
@@ -65,8 +65,12 @@ options:
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
|
||||
notes:
|
||||
- Supports C(check_mode).
|
||||
|
||||
seealso:
|
||||
- module: community.crypto.openssl_privatekey
|
||||
- module: community.crypto.openssl_privatekey_pipe
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
@@ -80,28 +84,28 @@ EXAMPLES = r'''
|
||||
register: result
|
||||
|
||||
- name: Dump information
|
||||
debug:
|
||||
ansible.builtin.debug:
|
||||
var: result
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
can_load_key:
|
||||
description: Whether the module was able to load the private key from disk
|
||||
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
|
||||
description: Whether the module was able to parse the private key.
|
||||
returned: always
|
||||
type: bool
|
||||
key_is_consistent:
|
||||
description:
|
||||
- Whether the key is consistent. Can also return C(none) next to C(yes) and
|
||||
C(no), to indicate that consistency couldn't be checked.
|
||||
C(no), to indicate that consistency could not be checked.
|
||||
- In case the check returns C(no), the module will fail.
|
||||
returned: always
|
||||
type: bool
|
||||
public_key:
|
||||
description: Private key's public key in PEM format
|
||||
description: Private key's public key in PEM format.
|
||||
returned: success
|
||||
type: str
|
||||
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
||||
@@ -583,7 +587,7 @@ def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
path=dict(type='path'),
|
||||
content=dict(type='str'),
|
||||
content=dict(type='str', no_log=True),
|
||||
passphrase=dict(type='str', no_log=True),
|
||||
return_private_key_data=dict(type='bool', default=False),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
|
||||
118
plugins/modules/openssl_privatekey_pipe.py
Normal file
118
plugins/modules/openssl_privatekey_pipe.py
Normal file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
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)
|
||||
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
|
||||
extends_documentation_fragment:
|
||||
- community.crypto.module_privatekey
|
||||
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:
|
||||
path: /etc/ssl/private/ansible.com.pem
|
||||
register: output
|
||||
no_log: true # make sure that private key data is not accidentally revealed in logs!
|
||||
- name: Show generated key
|
||||
debug:
|
||||
msg: "{{ output.privatekey }}"
|
||||
# DO NOT OUTPUT KEY MATERIAL TO CONSOLE OR LOGS IN PRODUCTION!
|
||||
|
||||
- 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.encrypt_sops:
|
||||
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
|
||||
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.
|
||||
- The PyOpenSSL backend requires PyOpenSSL >= 16.0 for meaningful output.
|
||||
returned: changed or success
|
||||
type: dict
|
||||
sample:
|
||||
md5: "84:75:71:72:8d:04:b5:6c:4d:37:6d:66:83:f5:4c:29"
|
||||
sha1: "51:cc:7c:68:5d:eb:41:43:88:7e:1a:ae:c7:f8:24:72:ee:71:f6:10"
|
||||
sha224: "b1:19:a6:6c:14:ac:33:1d:ed:18:50:d3:06:5c:b2:32:91:f1:f1:52:8c:cb:d5:75:e9:f5:9b:46"
|
||||
sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7"
|
||||
sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d"
|
||||
sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b"
|
||||
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
|
||||
'''
|
||||
@@ -92,10 +92,13 @@ extends_documentation_fragment:
|
||||
- files
|
||||
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'''
|
||||
@@ -395,7 +398,7 @@ def main():
|
||||
force=dict(type='bool', default=False),
|
||||
path=dict(type='path', required=True),
|
||||
privatekey_path=dict(type='path'),
|
||||
privatekey_content=dict(type='str'),
|
||||
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),
|
||||
|
||||
@@ -259,7 +259,7 @@ def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
privatekey_path=dict(type='path'),
|
||||
privatekey_content=dict(type='str'),
|
||||
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', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
|
||||
@@ -14,7 +14,7 @@ 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 via a certificate.
|
||||
- This module allows one to verify a signature for a file by a certificate.
|
||||
- The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL backend
|
||||
@@ -61,6 +61,7 @@ notes:
|
||||
RSA keys: C(cryptography) >= 1.4
|
||||
DSA and ECDSA keys: C(cryptography) >= 1.5
|
||||
ed448 and ed25519 keys: C(cryptography) >= 2.6
|
||||
- Supports C(check_mode).
|
||||
seealso:
|
||||
- module: community.crypto.openssl_signature
|
||||
- module: community.crypto.x509_certificate
|
||||
@@ -88,7 +89,7 @@ EXAMPLES = r'''
|
||||
|
||||
RETURN = r'''
|
||||
valid:
|
||||
description: C(true) means the signature was valid for the given file, C(false) means it wasn't.
|
||||
description: C(true) means the signature was valid for the given file, C(false) means it was not.
|
||||
returned: success
|
||||
type: bool
|
||||
'''
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,7 +54,7 @@ options:
|
||||
- Time can be specified either as relative time or as absolute timestamp.
|
||||
- Time will always be interpreted as UTC.
|
||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h), and ASN.1 TIME (i.e. pattern C(YYYYMMDDHHMMSSZ)).
|
||||
+ C([w | d | h | m | s]) (e.g. 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:
|
||||
@@ -70,10 +70,12 @@ options:
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
|
||||
notes:
|
||||
- All timestamp values are provided in ASN.1 TIME format, i.e. following the C(YYYYMMDDHHMMSSZ) pattern.
|
||||
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
|
||||
They are all in UTC.
|
||||
- Supports C(check_mode).
|
||||
seealso:
|
||||
- module: community.crypto.x509_certificate
|
||||
- module: community.crypto.x509_certificate_pipe
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
@@ -93,7 +95,7 @@ EXAMPLES = r'''
|
||||
register: result
|
||||
|
||||
- name: Dump information
|
||||
debug:
|
||||
ansible.builtin.debug:
|
||||
var: result
|
||||
|
||||
|
||||
@@ -119,7 +121,7 @@ EXAMPLES = r'''
|
||||
|
||||
RETURN = r'''
|
||||
expired:
|
||||
description: Whether the certificate is expired (i.e. C(notAfter) is in the past)
|
||||
description: Whether the certificate is expired (in other words, C(notAfter) is in the past).
|
||||
returned: success
|
||||
type: bool
|
||||
basic_constraints:
|
||||
@@ -143,7 +145,7 @@ extended_key_usage_critical:
|
||||
returned: success
|
||||
type: bool
|
||||
extensions_by_oid:
|
||||
description: Returns a dictionary for every extension OID
|
||||
description: Returns a dictionary for every extension OID.
|
||||
returned: success
|
||||
type: dict
|
||||
contains:
|
||||
@@ -152,7 +154,7 @@ extensions_by_oid:
|
||||
returned: success
|
||||
type: bool
|
||||
value:
|
||||
description: The Base64 encoded value (in DER format) of the extension
|
||||
description: The Base64 encoded value (in DER format) of the extension.
|
||||
returned: success
|
||||
type: str
|
||||
sample: "MAMCAQU="
|
||||
@@ -211,17 +213,17 @@ subject_ordered:
|
||||
elements: list
|
||||
sample: '[["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]'
|
||||
not_after:
|
||||
description: C(notAfter) date as ASN.1 TIME
|
||||
description: C(notAfter) date as ASN.1 TIME.
|
||||
returned: success
|
||||
type: str
|
||||
sample: 20190413202428Z
|
||||
not_before:
|
||||
description: C(notBefore) date as ASN.1 TIME
|
||||
description: C(notBefore) date as ASN.1 TIME.
|
||||
returned: success
|
||||
type: str
|
||||
sample: 20190331202428Z
|
||||
public_key:
|
||||
description: Certificate's public key in PEM format
|
||||
description: Certificate's public key in PEM format.
|
||||
returned: success
|
||||
type: str
|
||||
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
||||
@@ -233,6 +235,15 @@ public_key_fingerprints:
|
||||
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
|
||||
@@ -401,6 +412,10 @@ class CertificateInfo(OpenSSLObject):
|
||||
# Empty method because OpenSSLObject wants this
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_der_bytes(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_signature_algorithm(self):
|
||||
pass
|
||||
@@ -506,6 +521,8 @@ class CertificateInfo(OpenSSLObject):
|
||||
pk = self._get_public_key(binary=True)
|
||||
result['public_key_fingerprints'] = get_fingerprint_of_bytes(pk) if pk is not None else dict()
|
||||
|
||||
result['fingerprints'] = get_fingerprint_of_bytes(self._get_der_bytes())
|
||||
|
||||
if self.backend != 'pyopenssl':
|
||||
ski = self._get_subject_key_identifier()
|
||||
if ski is not None:
|
||||
@@ -533,6 +550,9 @@ class CertificateInfoCryptography(CertificateInfo):
|
||||
def __init__(self, module):
|
||||
super(CertificateInfoCryptography, self).__init__(module, 'cryptography')
|
||||
|
||||
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)
|
||||
|
||||
@@ -689,6 +709,9 @@ class CertificateInfoPyOpenSSL(CertificateInfo):
|
||||
def __init__(self, module):
|
||||
super(CertificateInfoPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
def _get_der_bytes(self):
|
||||
return crypto.dump_certificate(crypto.FILETYPE_ASN1, self.cert)
|
||||
|
||||
def _get_signature_algorithm(self):
|
||||
return to_text(self.cert.get_signature_algorithm())
|
||||
|
||||
|
||||
210
plugins/modules/x509_certificate_pipe.py
Normal file
210
plugins/modules/x509_certificate_pipe.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/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 COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
notes:
|
||||
- Supports C(check_mode).
|
||||
|
||||
extends_documentation_fragment:
|
||||
- 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
|
||||
'''
|
||||
|
||||
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
|
||||
'''
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from ansible.module_utils._text 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()
|
||||
@@ -15,7 +15,7 @@ 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 via serial number and (optionally) their issuer,
|
||||
- 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
|
||||
@@ -233,6 +233,7 @@ extends_documentation_fragment:
|
||||
notes:
|
||||
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
|
||||
- Date specified should be UTC. Minutes and seconds are mandatory.
|
||||
- Supports C(check_mode).
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
@@ -259,7 +260,7 @@ EXAMPLES = r'''
|
||||
|
||||
RETURN = r'''
|
||||
filename:
|
||||
description: Path to the generated CRL
|
||||
description: Path to the generated CRL.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: /path/to/my-ca.crl
|
||||
@@ -269,7 +270,7 @@ backup_file:
|
||||
type: str
|
||||
sample: /path/to/my-ca.crl.2019-03-09@11:22~
|
||||
privatekey:
|
||||
description: Path to the private CA key
|
||||
description: Path to the private CA key.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: /path/to/my-ca.pem
|
||||
@@ -404,7 +405,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
||||
cryptography_get_signature_algorithm_oid_from_crl,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.identify import (
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
identify_pem_format,
|
||||
)
|
||||
|
||||
@@ -501,7 +502,7 @@ class CRL(OpenSSLObject):
|
||||
result['serial_number'] = rc['serial_number']
|
||||
# All other options
|
||||
if rc['issuer']:
|
||||
result['issuer'] = [cryptography_get_name(issuer) for issuer in 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'],
|
||||
@@ -647,7 +648,7 @@ class CRL(OpenSSLObject):
|
||||
if entry['issuer'] is not None:
|
||||
revoked_cert = revoked_cert.add_extension(
|
||||
x509.CertificateIssuer([
|
||||
cryptography_get_name(name) for name in entry['issuer']
|
||||
cryptography_get_name(name, 'issuer') for name in entry['issuer']
|
||||
]),
|
||||
entry['issuer_critical']
|
||||
)
|
||||
@@ -754,7 +755,7 @@ def main():
|
||||
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'),
|
||||
privatekey_content=dict(type='str', no_log=True),
|
||||
privatekey_passphrase=dict(type='str', no_log=True),
|
||||
issuer=dict(type='dict'),
|
||||
last_update=dict(type='str', default='+0s'),
|
||||
|
||||
@@ -32,8 +32,9 @@ options:
|
||||
type: str
|
||||
|
||||
notes:
|
||||
- All timestamp values are provided in ASN.1 TIME format, i.e. following the C(YYYYMMDDHHMMSSZ) pattern.
|
||||
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
|
||||
They are all in UTC.
|
||||
- Supports C(check_mode).
|
||||
seealso:
|
||||
- module: community.crypto.x509_crl
|
||||
'''
|
||||
@@ -44,7 +45,8 @@ EXAMPLES = r'''
|
||||
path: /etc/ssl/my-ca.crl
|
||||
register: result
|
||||
|
||||
- debug:
|
||||
- name: Print the information
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ result }}"
|
||||
'''
|
||||
|
||||
@@ -158,7 +160,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
||||
cryptography_get_signature_algorithm_oid_from_crl,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.identify import (
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
identify_pem_format,
|
||||
)
|
||||
|
||||
|
||||
735
plugins/plugin_utils/action_module.py
Normal file
735
plugins/plugin_utils/action_module.py
Normal file
@@ -0,0 +1,735 @@
|
||||
# -*- 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>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# Parts taken from ansible.module_utils.basic and ansible.module_utils.common.warnings.
|
||||
|
||||
# NOTE: THIS MUST NOT BE USED BY A MODULE! 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 (
|
||||
handle_aliases,
|
||||
list_deprecations,
|
||||
list_no_log_values,
|
||||
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._text import to_native, to_text
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
|
||||
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._string_conversion_action = C.STRING_CONVERSION_ACTION
|
||||
|
||||
self.aliases = {}
|
||||
self._legal_inputs = []
|
||||
self._options_context = list()
|
||||
|
||||
self.params = copy.deepcopy(action_plugin._task.args)
|
||||
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 isn't 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.no_log_values = set()
|
||||
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': self._check_type_list,
|
||||
'dict': self._check_type_dict,
|
||||
'bool': self._check_type_bool,
|
||||
'int': self._check_type_int,
|
||||
'float': self._check_type_float,
|
||||
'path': self._check_type_path,
|
||||
'raw': self._check_type_raw,
|
||||
'jsonarg': self._check_type_jsonarg,
|
||||
'json': self._check_type_jsonarg,
|
||||
'bytes': self._check_type_bytes,
|
||||
'bits': self._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)
|
||||
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))
|
||||
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):
|
||||
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 can't 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(self._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 self._string_conversion_action == 'error':
|
||||
msg = common_msg.capitalize()
|
||||
raise TypeError(to_native(msg))
|
||||
elif self._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 _check_type_list(self, value):
|
||||
return check_type_list(value)
|
||||
|
||||
def _check_type_dict(self, value):
|
||||
return check_type_dict(value)
|
||||
|
||||
def _check_type_bool(self, value):
|
||||
return check_type_bool(value)
|
||||
|
||||
def _check_type_int(self, value):
|
||||
return check_type_int(value)
|
||||
|
||||
def _check_type_float(self, value):
|
||||
return check_type_float(value)
|
||||
|
||||
def _check_type_path(self, value):
|
||||
return check_type_path(value)
|
||||
|
||||
def _check_type_jsonarg(self, value):
|
||||
return check_type_jsonarg(value)
|
||||
|
||||
def _check_type_raw(self, value):
|
||||
return check_type_raw(value)
|
||||
|
||||
def _check_type_bytes(self, value):
|
||||
return check_type_bytes(value)
|
||||
|
||||
def _check_type_bits(self, value):
|
||||
return check_type_bits(value)
|
||||
|
||||
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 doesn't 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
|
||||
@@ -1,78 +0,0 @@
|
||||
language: python
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- T=none
|
||||
|
||||
matrix:
|
||||
exclude:
|
||||
- env: T=none
|
||||
include:
|
||||
- env: T=devel/sanity/1
|
||||
- env: T=devel/sanity/extra
|
||||
|
||||
- env: T=devel/units/2.6/1
|
||||
- env: T=devel/units/2.7/1
|
||||
- env: T=devel/units/3.5/1
|
||||
- env: T=devel/units/3.6/1
|
||||
- env: T=devel/units/3.7/1
|
||||
- env: T=devel/units/3.8/1
|
||||
- env: T=devel/units/3.9/1
|
||||
|
||||
- env: T=devel/osx/10.11/1
|
||||
- env: T=devel/rhel/7.8/1
|
||||
- env: T=devel/rhel/8.2/1
|
||||
- env: T=devel/freebsd/11.1/1
|
||||
- env: T=devel/freebsd/12.1/1
|
||||
- env: T=devel/linux/centos6/1
|
||||
- env: T=devel/linux/centos7/1
|
||||
- env: T=devel/linux/centos8/1
|
||||
- env: T=devel/linux/fedora30/1
|
||||
- env: T=devel/linux/fedora31/1
|
||||
- env: T=devel/linux/fedora32/1
|
||||
- env: T=devel/linux/opensuse15py2/1
|
||||
- env: T=devel/linux/opensuse15/1
|
||||
- env: T=devel/linux/ubuntu1604/1
|
||||
- env: T=devel/linux/ubuntu1804/1
|
||||
|
||||
- env: T=devel/cloud/2.6/1
|
||||
- env: T=devel/cloud/2.7/1
|
||||
- env: T=devel/cloud/3.5/1
|
||||
- env: T=devel/cloud/3.6/1
|
||||
- env: T=devel/cloud/3.7/1
|
||||
- env: T=devel/cloud/3.8/1
|
||||
- env: T=devel/cloud/3.9/1
|
||||
|
||||
# For Ansible 2.10, use a combination of different targets
|
||||
- env: T=2.10/sanity/1
|
||||
- env: T=2.10/units/2.7/1
|
||||
- env: T=2.10/units/3.8/1
|
||||
- env: T=2.10/rhel/7.8/1
|
||||
- env: T=2.10/linux/ubuntu1804/1
|
||||
- env: T=2.10/cloud/3.6/1
|
||||
|
||||
# For Ansible 2.9, use a combination of different targets
|
||||
- env: T=2.9/sanity/1
|
||||
- env: T=2.9/units/2.7/1
|
||||
- env: T=2.9/units/3.8/1
|
||||
- env: T=2.9/rhel/7.8/1
|
||||
- env: T=2.9/linux/ubuntu1804/1
|
||||
- env: T=2.9/cloud/3.5/1
|
||||
|
||||
branches:
|
||||
except:
|
||||
- "*-patch-*"
|
||||
- "revert-*-*"
|
||||
|
||||
build:
|
||||
ci:
|
||||
- tests/utils/shippable/timing.sh tests/utils/shippable/shippable.sh $T
|
||||
|
||||
integrations:
|
||||
notifications:
|
||||
- integrationName: email
|
||||
type: email
|
||||
on_success: never
|
||||
on_failure: never
|
||||
on_start: never
|
||||
on_pull_request: never
|
||||
@@ -1,5 +1,5 @@
|
||||
- name: Generate account keys
|
||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/{{ item }}.pem
|
||||
command: "{{ openssl_binary }} ecparam -name prime256v1 -genkey -out {{ output_dir }}/{{ item }}.pem"
|
||||
loop:
|
||||
- accountkey
|
||||
- accountkey2
|
||||
@@ -8,7 +8,7 @@
|
||||
- accountkey5
|
||||
|
||||
- name: Parse account keys (to ease debugging some test failures)
|
||||
command: openssl ec -in {{ output_dir }}/{{ item }}.pem -noout -text
|
||||
command: "{{ openssl_binary }} ec -in {{ output_dir }}/{{ item }}.pem -noout -text"
|
||||
loop:
|
||||
- accountkey
|
||||
- accountkey2
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
---
|
||||
####################################################################
|
||||
# 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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
- name: Generate account key
|
||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem
|
||||
command: "{{ openssl_binary }} ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem"
|
||||
|
||||
- name: Generate second account key
|
||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey2.pem
|
||||
command: "{{ openssl_binary }} ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey2.pem"
|
||||
|
||||
- name: Parse account key (to ease debugging some test failures)
|
||||
command: openssl ec -in {{ output_dir }}/accountkey.pem -noout -text
|
||||
command: "{{ openssl_binary }} ec -in {{ output_dir }}/accountkey.pem -noout -text"
|
||||
|
||||
- name: Check that account does not exist
|
||||
acme_account_info:
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
---
|
||||
####################################################################
|
||||
# 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
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
## SET UP ACCOUNT KEYS ########################################################################
|
||||
- name: Create ECC256 account key
|
||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem
|
||||
command: "{{ openssl_binary }} ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem"
|
||||
- name: Create ECC384 account key
|
||||
command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem
|
||||
- name: Create RSA-2048 account key
|
||||
command: openssl genrsa -out {{ output_dir }}/account-rsa2048.pem 2048
|
||||
command: "{{ openssl_binary }} ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem"
|
||||
- name: Create RSA account key
|
||||
command: "{{ openssl_binary }} genrsa -out {{ output_dir }}/account-rsa.pem {{ default_rsa_key_size }}"
|
||||
## SET UP ACCOUNTS ############################################################################
|
||||
- name: Make sure ECC256 account hasn't been created yet
|
||||
acme_account:
|
||||
@@ -28,13 +28,13 @@
|
||||
contact:
|
||||
- mailto:example@example.org
|
||||
- mailto:example@example.com
|
||||
- name: Create RSA-2048 account
|
||||
- name: Create RSA account
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
account_key_src: "{{ output_dir }}/account-rsa2048.pem"
|
||||
account_key_src: "{{ output_dir }}/account-rsa.pem"
|
||||
state: present
|
||||
allow_creation: yes
|
||||
terms_agreed: yes
|
||||
@@ -46,7 +46,7 @@
|
||||
certgen_title: Certificate 1
|
||||
certificate_name: cert-1
|
||||
key_type: rsa
|
||||
rsa_bits: 2048
|
||||
rsa_bits: "{{ default_rsa_key_size }}"
|
||||
subject_alt_name: "DNS:example.com"
|
||||
subject_alt_name_critical: no
|
||||
account_key: account-ec256
|
||||
@@ -62,6 +62,7 @@
|
||||
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 }}"
|
||||
@@ -93,6 +94,7 @@
|
||||
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 }}"
|
||||
@@ -105,7 +107,7 @@
|
||||
key_type: ec384
|
||||
subject_alt_name: "DNS:*.example.com,DNS:example.org,DNS:t1.example.com"
|
||||
subject_alt_name_critical: no
|
||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa2048.pem') }}"
|
||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa.pem') }}"
|
||||
challenge: dns-01
|
||||
modify_account: no
|
||||
deactivate_authzs: no
|
||||
@@ -118,6 +120,7 @@
|
||||
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 }}"
|
||||
@@ -128,10 +131,10 @@
|
||||
certgen_title: Certificate 4
|
||||
certificate_name: cert-4
|
||||
key_type: rsa
|
||||
rsa_bits: 2048
|
||||
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: no
|
||||
account_key: account-rsa2048
|
||||
account_key: account-rsa
|
||||
challenge: http-01
|
||||
modify_account: no
|
||||
deactivate_authzs: yes
|
||||
@@ -145,6 +148,7 @@
|
||||
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 }}"
|
||||
@@ -165,6 +169,7 @@
|
||||
remaining_days: 10
|
||||
terms_agreed: no
|
||||
account_email: ""
|
||||
use_csr_content: true
|
||||
- name: Store obtain results for cert 5a
|
||||
set_fact:
|
||||
cert_5a_obtain_results: "{{ certificate_obtain_result }}"
|
||||
@@ -185,6 +190,7 @@
|
||||
remaining_days: 10
|
||||
terms_agreed: no
|
||||
account_email: ""
|
||||
use_csr_content: false
|
||||
- name: Store obtain results for cert 5b
|
||||
set_fact:
|
||||
cert_5_recreate_1: "{{ challenge_data is changed }}"
|
||||
@@ -204,6 +210,7 @@
|
||||
remaining_days: 1000
|
||||
terms_agreed: no
|
||||
account_email: ""
|
||||
use_csr_content: true
|
||||
- name: Store obtain results for cert 5c
|
||||
set_fact:
|
||||
cert_5_recreate_2: "{{ challenge_data is changed }}"
|
||||
@@ -224,6 +231,7 @@
|
||||
remaining_days: 10
|
||||
terms_agreed: no
|
||||
account_email: ""
|
||||
use_csr_content: false
|
||||
- name: Store obtain results for cert 5d
|
||||
set_fact:
|
||||
cert_5_recreate_3: "{{ challenge_data is changed }}"
|
||||
@@ -234,7 +242,7 @@
|
||||
certgen_title: Certificate 6
|
||||
certificate_name: cert-6
|
||||
key_type: rsa
|
||||
rsa_bits: 2048
|
||||
rsa_bits: "{{ default_rsa_key_size }}"
|
||||
subject_alt_name: "DNS:example.org"
|
||||
subject_alt_name_critical: no
|
||||
account_key: account-ec256
|
||||
@@ -255,6 +263,7 @@
|
||||
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 }}"
|
||||
@@ -265,7 +274,7 @@
|
||||
certgen_title: Certificate 7
|
||||
certificate_name: cert-7
|
||||
key_type: rsa
|
||||
rsa_bits: 2048
|
||||
rsa_bits: "{{ default_rsa_key_size }}"
|
||||
subject_alt_name:
|
||||
- "IP:127.0.0.1"
|
||||
# - "IP:::1"
|
||||
@@ -282,6 +291,7 @@
|
||||
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 }}"
|
||||
@@ -292,7 +302,7 @@
|
||||
certgen_title: Certificate 8
|
||||
certificate_name: cert-8
|
||||
key_type: rsa
|
||||
rsa_bits: 2048
|
||||
rsa_bits: "{{ default_rsa_key_size }}"
|
||||
subject_alt_name:
|
||||
- "IP:127.0.0.1"
|
||||
# IPv4 only since our test validation server doesn't work
|
||||
@@ -307,6 +317,7 @@
|
||||
remaining_days: 10
|
||||
terms_agreed: yes
|
||||
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 }}"
|
||||
@@ -314,61 +325,61 @@
|
||||
## DISSECT CERTIFICATES #######################################################################
|
||||
# Make sure certificates are valid. Root certificate for Pebble equals the chain certificate.
|
||||
- name: Verifying cert 1
|
||||
command: openssl verify -CAfile "{{ output_dir }}/cert-1-root.pem" -untrusted "{{ output_dir }}/cert-1-chain.pem" "{{ output_dir }}/cert-1.pem"
|
||||
command: '{{ openssl_binary }} verify -CAfile "{{ output_dir }}/cert-1-root.pem" -untrusted "{{ output_dir }}/cert-1-chain.pem" "{{ output_dir }}/cert-1.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_1_valid
|
||||
- name: Verifying cert 2
|
||||
command: openssl verify -CAfile "{{ output_dir }}/cert-2-root.pem" -untrusted "{{ output_dir }}/cert-2-chain.pem" "{{ output_dir }}/cert-2.pem"
|
||||
command: '{{ openssl_binary }} verify -CAfile "{{ output_dir }}/cert-2-root.pem" -untrusted "{{ output_dir }}/cert-2-chain.pem" "{{ output_dir }}/cert-2.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_2_valid
|
||||
- name: Verifying cert 3
|
||||
command: openssl verify -CAfile "{{ output_dir }}/cert-3-root.pem" -untrusted "{{ output_dir }}/cert-3-chain.pem" "{{ output_dir }}/cert-3.pem"
|
||||
command: '{{ openssl_binary }} verify -CAfile "{{ output_dir }}/cert-3-root.pem" -untrusted "{{ output_dir }}/cert-3-chain.pem" "{{ output_dir }}/cert-3.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_3_valid
|
||||
- name: Verifying cert 4
|
||||
command: openssl verify -CAfile "{{ output_dir }}/cert-4-root.pem" -untrusted "{{ output_dir }}/cert-4-chain.pem" "{{ output_dir }}/cert-4.pem"
|
||||
command: '{{ openssl_binary }} verify -CAfile "{{ output_dir }}/cert-4-root.pem" -untrusted "{{ output_dir }}/cert-4-chain.pem" "{{ output_dir }}/cert-4.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_4_valid
|
||||
- name: Verifying cert 5
|
||||
command: openssl verify -CAfile "{{ output_dir }}/cert-5-root.pem" -untrusted "{{ output_dir }}/cert-5-chain.pem" "{{ output_dir }}/cert-5.pem"
|
||||
command: '{{ openssl_binary }} verify -CAfile "{{ output_dir }}/cert-5-root.pem" -untrusted "{{ output_dir }}/cert-5-chain.pem" "{{ output_dir }}/cert-5.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_5_valid
|
||||
- name: Verifying cert 6
|
||||
command: openssl verify -CAfile "{{ output_dir }}/cert-6-root.pem" -untrusted "{{ output_dir }}/cert-6-chain.pem" "{{ output_dir }}/cert-6.pem"
|
||||
command: '{{ openssl_binary }} verify -CAfile "{{ output_dir }}/cert-6-root.pem" -untrusted "{{ output_dir }}/cert-6-chain.pem" "{{ output_dir }}/cert-6.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_6_valid
|
||||
- name: Verifying cert 7
|
||||
command: openssl verify -CAfile "{{ output_dir }}/cert-7-root.pem" -untrusted "{{ output_dir }}/cert-7-chain.pem" "{{ output_dir }}/cert-7.pem"
|
||||
command: '{{ openssl_binary }} verify -CAfile "{{ output_dir }}/cert-7-root.pem" -untrusted "{{ output_dir }}/cert-7-chain.pem" "{{ output_dir }}/cert-7.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_7_valid
|
||||
- name: Verifying cert 8
|
||||
command: openssl verify -CAfile "{{ output_dir }}/cert-8-root.pem" -untrusted "{{ output_dir }}/cert-8-chain.pem" "{{ output_dir }}/cert-8.pem"
|
||||
command: '{{ openssl_binary }} verify -CAfile "{{ output_dir }}/cert-8-root.pem" -untrusted "{{ output_dir }}/cert-8-chain.pem" "{{ output_dir }}/cert-8.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_8_valid
|
||||
# Dump certificate info
|
||||
- name: Dumping cert 1
|
||||
command: openssl x509 -in "{{ output_dir }}/cert-1.pem" -noout -text
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-1.pem" -noout -text'
|
||||
register: cert_1_text
|
||||
- name: Dumping cert 2
|
||||
command: openssl x509 -in "{{ output_dir }}/cert-2.pem" -noout -text
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-2.pem" -noout -text'
|
||||
register: cert_2_text
|
||||
- name: Dumping cert 3
|
||||
command: openssl x509 -in "{{ output_dir }}/cert-3.pem" -noout -text
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-3.pem" -noout -text'
|
||||
register: cert_3_text
|
||||
- name: Dumping cert 4
|
||||
command: openssl x509 -in "{{ output_dir }}/cert-4.pem" -noout -text
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-4.pem" -noout -text'
|
||||
register: cert_4_text
|
||||
- name: Dumping cert 5
|
||||
command: openssl x509 -in "{{ output_dir }}/cert-5.pem" -noout -text
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-5.pem" -noout -text'
|
||||
register: cert_5_text
|
||||
- name: Dumping cert 6
|
||||
command: openssl x509 -in "{{ output_dir }}/cert-6.pem" -noout -text
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-6.pem" -noout -text'
|
||||
register: cert_6_text
|
||||
- name: Dumping cert 7
|
||||
command: openssl x509 -in "{{ output_dir }}/cert-7.pem" -noout -text
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-7.pem" -noout -text'
|
||||
register: cert_7_text
|
||||
- name: Dumping cert 8
|
||||
command: openssl x509 -in "{{ output_dir }}/cert-8.pem" -noout -text
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-8.pem" -noout -text'
|
||||
register: cert_8_text
|
||||
# Dump certificate info
|
||||
- name: Dumping cert 1
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
---
|
||||
####################################################################
|
||||
# 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:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
## SET UP ACCOUNT KEYS ########################################################################
|
||||
- name: Create ECC256 account key
|
||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem
|
||||
command: "{{ openssl_binary }} ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem"
|
||||
- name: Create ECC384 account key
|
||||
command: openssl ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem
|
||||
- name: Create RSA-2048 account key
|
||||
command: openssl genrsa -out {{ output_dir }}/account-rsa2048.pem 2048
|
||||
command: "{{ openssl_binary }} ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem"
|
||||
- name: Create RSA account key
|
||||
command: "{{ openssl_binary }} genrsa -out {{ output_dir }}/account-rsa.pem {{ default_rsa_key_size }}"
|
||||
## CREATE ACCOUNTS AND OBTAIN CERTIFICATES ####################################################
|
||||
- name: Obtain cert 1
|
||||
include_tasks: obtain-cert.yml
|
||||
@@ -13,7 +13,7 @@
|
||||
certgen_title: Certificate 1 for revocation
|
||||
certificate_name: cert-1
|
||||
key_type: rsa
|
||||
rsa_bits: 2048
|
||||
rsa_bits: "{{ default_rsa_key_size }}"
|
||||
subject_alt_name: "DNS:example.com"
|
||||
subject_alt_name_critical: no
|
||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec256.pem') }}"
|
||||
@@ -48,7 +48,7 @@
|
||||
key_type: ec384
|
||||
subject_alt_name: "DNS:t1.example.com"
|
||||
subject_alt_name_critical: no
|
||||
account_key: account-rsa2048
|
||||
account_key: account-rsa
|
||||
challenge: dns-01
|
||||
modify_account: yes
|
||||
deactivate_authzs: no
|
||||
@@ -80,7 +80,7 @@
|
||||
- name: Revoke certificate 3 via account key (fullchain)
|
||||
acme_certificate_revoke:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa2048.pem') }}"
|
||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa.pem') }}"
|
||||
certificate: "{{ output_dir }}/cert-3-fullchain.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
---
|
||||
####################################################################
|
||||
# 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
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
---
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
- block:
|
||||
- name: Create ECC256 account key
|
||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem
|
||||
command: "{{ openssl_binary }} ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem"
|
||||
- name: Obtain cert 1
|
||||
include_tasks: obtain-cert.yml
|
||||
vars:
|
||||
@@ -9,7 +14,7 @@
|
||||
certgen_title: Certificate 1
|
||||
certificate_name: cert-1
|
||||
key_type: rsa
|
||||
rsa_bits: 2048
|
||||
rsa_bits: "{{ default_rsa_key_size }}"
|
||||
subject_alt_name: "DNS:example.com"
|
||||
subject_alt_name_critical: no
|
||||
account_key: account-ec256
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
- name: Generate account key
|
||||
command: openssl ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem
|
||||
command: "{{ openssl_binary }} ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem"
|
||||
|
||||
- name: Parse account key (to ease debugging some test failures)
|
||||
command: openssl ec -in {{ output_dir }}/accountkey.pem -noout -text
|
||||
command: "{{ openssl_binary }} ec -in {{ output_dir }}/accountkey.pem -noout -text"
|
||||
|
||||
- name: Get directory
|
||||
acme_inspect:
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
---
|
||||
####################################################################
|
||||
# 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
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
- name: register cryptography version
|
||||
command: '{{ ansible_python.executable }} -c ''import cryptography; print(cryptography.__version__)'''
|
||||
register: cryptography_version
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
---
|
||||
####################################################################
|
||||
# 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:
|
||||
@@ -201,7 +206,7 @@
|
||||
|
||||
# For bug 61738, verify that the full chain is valid
|
||||
- name: Verify that the full chain path can be successfully imported
|
||||
command: openssl verify "{{ example4_full_chain_path }}"
|
||||
command: '{{ openssl_binary }} verify "{{ example4_full_chain_path }}"'
|
||||
register: openssl_result
|
||||
|
||||
- assert:
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
---
|
||||
####################################################################
|
||||
# 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:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
dependencies:
|
||||
- setup_openssl
|
||||
- setup_pyopenssl
|
||||
- prepare_http_tests
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
---
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
- block:
|
||||
|
||||
- name: Get servers certificate with backend auto-detection
|
||||
|
||||
@@ -1,4 +1,38 @@
|
||||
---
|
||||
- name: Get servers certificate for SNI test part 1
|
||||
get_certificate:
|
||||
host: "{{ httpbin_host }}"
|
||||
port: 443
|
||||
server_name: "{{ sni_host }}"
|
||||
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 }}"
|
||||
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 }}"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
shippable/posix/group1
|
||||
skip/osx
|
||||
skip/macos
|
||||
skip/freebsd
|
||||
skip/docker
|
||||
needs/root
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
---
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
- name: Make sure cryptsetup is installed
|
||||
package:
|
||||
name: cryptsetup
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
check_mode: yes
|
||||
become: yes
|
||||
register: create_check
|
||||
@@ -12,6 +14,8 @@
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
register: create
|
||||
- name: Create (idempotent)
|
||||
@@ -19,6 +23,8 @@
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
register: create_idem
|
||||
- name: Create (idempotent, check)
|
||||
@@ -26,6 +32,8 @@
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
check_mode: yes
|
||||
become: yes
|
||||
register: create_idem_check
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
device: /dev/asdfasdfasdf
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
check_mode: yes
|
||||
ignore_errors: yes
|
||||
become: yes
|
||||
@@ -13,6 +15,8 @@
|
||||
device: /dev/asdfasdfasdf
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
ignore_errors: yes
|
||||
become: yes
|
||||
register: create
|
||||
@@ -28,6 +32,8 @@
|
||||
device: /tmp/
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
check_mode: yes
|
||||
ignore_errors: yes
|
||||
become: yes
|
||||
@@ -37,6 +43,8 @@
|
||||
device: /tmp/
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
ignore_errors: yes
|
||||
become: yes
|
||||
register: create
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
|
||||
# Access: keyfile1
|
||||
@@ -43,7 +45,24 @@
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
new_keyfile: "{{ role_path }}/files/keyfile2"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
register: result_1
|
||||
|
||||
- name: Give access to keyfile2 (idempotent)
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
new_keyfile: "{{ role_path }}/files/keyfile2"
|
||||
become: yes
|
||||
register: result_2
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result_1 is changed
|
||||
- result_2 is not changed
|
||||
|
||||
# Access: keyfile1 and keyfile2
|
||||
|
||||
@@ -75,6 +94,21 @@
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
remove_keyfile: "{{ role_path }}/files/keyfile1"
|
||||
become: yes
|
||||
register: result_1
|
||||
|
||||
- name: Remove access from keyfile1 (idempotent)
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
remove_keyfile: "{{ role_path }}/files/keyfile1"
|
||||
become: yes
|
||||
register: result_2
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result_1 is changed
|
||||
- result_2 is not changed
|
||||
|
||||
# Access: keyfile2
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keysize: 256
|
||||
pbkdf:
|
||||
iteration_count: 1000
|
||||
become: yes
|
||||
register: create_with_keysize
|
||||
- name: Create with keysize (idempotent)
|
||||
@@ -13,6 +15,8 @@
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keysize: 256
|
||||
pbkdf:
|
||||
iteration_count: 1000
|
||||
become: yes
|
||||
register: create_idem_with_keysize
|
||||
- name: Create with different keysize (idempotent since we do not update keysize)
|
||||
@@ -21,6 +25,8 @@
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keysize: 512
|
||||
pbkdf:
|
||||
iteration_count: 1000
|
||||
become: yes
|
||||
register: create_idem_with_diff_keysize
|
||||
- name: Create with ambiguous arguments
|
||||
@@ -29,6 +35,8 @@
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
pbkdf:
|
||||
iteration_count: 1000
|
||||
ignore_errors: yes
|
||||
become: yes
|
||||
register: create_with_ambiguous
|
||||
|
||||
@@ -4,7 +4,29 @@
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
algorithm: argon2i
|
||||
memory: 1000
|
||||
parallel: 1
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
register: create_passphrase_1
|
||||
|
||||
- name: Make sure that the previous task only fails because the LUKS version used cannot handle the PBKDF parameters
|
||||
assert:
|
||||
that:
|
||||
- create_passphrase_1 is not failed or 'Failed to set pbkdf parameters' in create_passphrase_1.msg
|
||||
|
||||
- name: Create with passphrase1 (without argon2i)
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
when: create_passphrase_1 is failed and 'Failed to set pbkdf parameters' in create_passphrase_1.msg
|
||||
|
||||
- name: Open with passphrase1
|
||||
luks_device:
|
||||
@@ -30,6 +52,8 @@
|
||||
passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
new_passphrase: "{{ cryptfile_passphrase2 }}"
|
||||
new_keyfile: "{{ role_path }}/files/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
register: new_try
|
||||
@@ -55,7 +79,24 @@
|
||||
state: closed
|
||||
passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
new_passphrase: "{{ cryptfile_passphrase2 }}"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
register: result_1
|
||||
|
||||
- name: Give access to passphrase2 (idempotent)
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
new_passphrase: "{{ cryptfile_passphrase2 }}"
|
||||
become: yes
|
||||
register: result_2
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result_1 is changed
|
||||
- result_2 is not changed
|
||||
|
||||
- name: Open with passphrase2
|
||||
luks_device:
|
||||
@@ -92,6 +133,8 @@
|
||||
state: closed
|
||||
passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
new_keyfile: "{{ role_path }}/files/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
|
||||
- name: Remove access with ambiguous remove_ arguments
|
||||
@@ -130,6 +173,20 @@
|
||||
state: closed
|
||||
remove_passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
become: yes
|
||||
register: result_1
|
||||
|
||||
- name: Remove access for passphrase1 (idempotent)
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
remove_passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
become: yes
|
||||
register: result_2
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result_1 is changed
|
||||
- result_2 is not changed
|
||||
|
||||
- name: Try to open with passphrase1
|
||||
luks_device:
|
||||
@@ -161,6 +218,8 @@
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
new_passphrase: "{{ cryptfile_passphrase3 }}"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
|
||||
- name: Open with passphrase3
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
dependencies:
|
||||
- setup_ssh_keygen
|
||||
- setup_ssh_agent
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
- name: openssh_cert integration tests
|
||||
when: not (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] == "6")
|
||||
block:
|
||||
- name: Generate keypair (check mode)
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/id_key'
|
||||
type: rsa
|
||||
check_mode: yes
|
||||
- name: Generate keypair
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/id_key'
|
||||
type: rsa
|
||||
- name: Generate keypair (idempotent)
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/id_key'
|
||||
type: rsa
|
||||
- name: Generate keypair (idempotent, check mode)
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/id_key'
|
||||
type: rsa
|
||||
check_mode: yes
|
||||
size: 2048
|
||||
- name: Generate always valid cert (check mode)
|
||||
openssh_cert:
|
||||
type: user
|
||||
@@ -388,21 +380,88 @@
|
||||
#valid_from: "2001-01-21"
|
||||
#valid_to: "2019-01-21"
|
||||
check_mode: yes
|
||||
- name: Remove keypair (check mode)
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/id_key'
|
||||
state: absent
|
||||
check_mode: yes
|
||||
- name: Remove keypair
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/id_key'
|
||||
state: absent
|
||||
- name: Remove keypair (idempotent)
|
||||
|
||||
- name: openssh_cert integration tests that require ssh-agent
|
||||
when: openssh_version is version("7.6",">=")
|
||||
environment:
|
||||
SSH_AUTH_SOCK: "{{ openssh_agent_sock }}"
|
||||
block:
|
||||
- name: Generate keypair for agent tests
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/id_key'
|
||||
state: absent
|
||||
- name: Remove keypair (idempotent, check mode)
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/id_key'
|
||||
state: absent
|
||||
type: rsa
|
||||
size: 2048
|
||||
- name: Generate always valid cert using agent without key in agent (should fail)
|
||||
openssh_cert:
|
||||
type: user
|
||||
signing_key: '{{ output_dir }}/id_key'
|
||||
public_key: '{{ output_dir }}/id_key.pub'
|
||||
path: '{{ output_dir }}/id_cert_with_agent'
|
||||
use_agent: yes
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
register: rc_no_key_in_agent
|
||||
ignore_errors: yes
|
||||
- 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 {{ output_dir }}/id_key'
|
||||
- name: Generate always valid cert with agent (check mode)
|
||||
openssh_cert:
|
||||
type: user
|
||||
signing_key: '{{ output_dir }}/id_key'
|
||||
public_key: '{{ output_dir }}/id_key.pub'
|
||||
path: '{{ output_dir }}/id_cert_with_agent'
|
||||
use_agent: yes
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
check_mode: yes
|
||||
- name: Generate always valid cert with agent
|
||||
openssh_cert:
|
||||
type: user
|
||||
signing_key: '{{ output_dir }}/id_key'
|
||||
public_key: '{{ output_dir }}/id_key.pub'
|
||||
path: '{{ output_dir }}/id_cert_with_agent'
|
||||
use_agent: yes
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
- name: Generate always valid cert with agent (idempotent)
|
||||
openssh_cert:
|
||||
type: user
|
||||
signing_key: '{{ output_dir }}/id_key'
|
||||
public_key: '{{ output_dir }}/id_key.pub'
|
||||
path: '{{ output_dir }}/id_cert_with_agent'
|
||||
use_agent: yes
|
||||
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: '{{ output_dir }}/id_key'
|
||||
public_key: '{{ output_dir }}/id_key.pub'
|
||||
path: '{{ output_dir }}/id_cert_with_agent'
|
||||
use_agent: yes
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
check_mode: yes
|
||||
- name: Remove keypair for agent tests
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/id_key'
|
||||
state: absent
|
||||
- name: Remove certificate
|
||||
openssh_cert:
|
||||
state: absent
|
||||
path: '{{ output_dir }}/id_cert_with_agent'
|
||||
|
||||
@@ -1,18 +1,38 @@
|
||||
---
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
- name: Generate privatekey1 - standard (check mode)
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey1'
|
||||
size: 2048
|
||||
register: privatekey1_result_check
|
||||
check_mode: true
|
||||
|
||||
- name: Generate privatekey1 - standard
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey1'
|
||||
size: 2048
|
||||
register: privatekey1_result
|
||||
|
||||
- name: Generate privatekey1 - standard (check mode idempotent)
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey1'
|
||||
size: 2048
|
||||
register: privatekey1_idem_result_check
|
||||
check_mode: true
|
||||
|
||||
- name: Generate privatekey1 - standard (idempotent)
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey1'
|
||||
size: 2048
|
||||
register: privatekey1_idem_result
|
||||
|
||||
- name: Generate privatekey2 - size 2048
|
||||
- name: Generate privatekey2 - default size
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey2'
|
||||
size: 2048
|
||||
|
||||
- name: Generate privatekey3 - type dsa
|
||||
openssh_keypair:
|
||||
@@ -22,6 +42,7 @@
|
||||
- name: Generate privatekey4 - standard
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey4'
|
||||
size: 2048
|
||||
|
||||
- name: Delete privatekey4 - standard
|
||||
openssh_keypair:
|
||||
@@ -31,17 +52,20 @@
|
||||
- name: Generate privatekey5 - standard
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey5'
|
||||
size: 2048
|
||||
register: publickey_gen
|
||||
|
||||
- name: Generate privatekey6
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey6'
|
||||
type: rsa
|
||||
size: 2048
|
||||
|
||||
- name: Regenerate privatekey6 via force
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey6'
|
||||
type: rsa
|
||||
size: 2048
|
||||
force: yes
|
||||
register: output_regenerated_via_force
|
||||
|
||||
@@ -58,6 +82,7 @@
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekeybroken'
|
||||
type: rsa
|
||||
size: 2048
|
||||
register: output_broken
|
||||
ignore_errors: yes
|
||||
|
||||
@@ -66,6 +91,7 @@
|
||||
path: '{{ output_dir }}/privatekeybroken'
|
||||
type: rsa
|
||||
force: yes
|
||||
size: 2048
|
||||
register: output_broken_force
|
||||
|
||||
- name: Generate read-only private key
|
||||
@@ -73,24 +99,28 @@
|
||||
path: '{{ output_dir }}/privatekeyreadonly'
|
||||
type: rsa
|
||||
mode: '0200'
|
||||
size: 2048
|
||||
|
||||
- name: Regenerate read-only private key via force
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekeyreadonly'
|
||||
type: rsa
|
||||
force: yes
|
||||
size: 2048
|
||||
register: output_read_only
|
||||
|
||||
- name: Generate privatekey7 - standard with comment
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey7'
|
||||
comment: 'test@privatekey7'
|
||||
size: 2048
|
||||
register: privatekey7_result
|
||||
|
||||
- name: Modify privatekey7 comment
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey7'
|
||||
comment: 'test_modified@privatekey7'
|
||||
size: 2048
|
||||
register: privatekey7_modified_result
|
||||
|
||||
- name: Generate password protected key
|
||||
@@ -99,6 +129,7 @@
|
||||
- name: Try to modify the password protected key - should fail
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey8'
|
||||
size: 2048
|
||||
register: privatekey8_result
|
||||
ignore_errors: yes
|
||||
|
||||
@@ -106,6 +137,7 @@
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey8'
|
||||
force: yes
|
||||
size: 2048
|
||||
register: privatekey8_result_force
|
||||
|
||||
- import_tasks: ../tests/validate.yml
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
debug:
|
||||
var: privatekey1_result
|
||||
|
||||
- name: Validate general behavior
|
||||
assert:
|
||||
that:
|
||||
- privatekey1_result_check is changed
|
||||
- privatekey1_result is changed
|
||||
- privatekey1_idem_result_check is not changed
|
||||
- privatekey1_idem_result is not changed
|
||||
|
||||
- name: Validate privatekey1 return fingerprint
|
||||
assert:
|
||||
that:
|
||||
@@ -21,7 +29,7 @@
|
||||
assert:
|
||||
that:
|
||||
- privatekey1_result["size"]|type_debug == 'int'
|
||||
- privatekey1_result["size"] == 4096
|
||||
- privatekey1_result["size"] == 2048
|
||||
|
||||
- name: Validate privatekey1 return key type
|
||||
assert:
|
||||
@@ -29,14 +37,14 @@
|
||||
- privatekey1_result["type"] is string
|
||||
- privatekey1_result["type"] == "rsa"
|
||||
|
||||
- name: Validate privatekey1 (test - RSA key with size 4096 bits)
|
||||
- name: Validate privatekey1 (test - RSA key with size 2048 bits)
|
||||
shell: "ssh-keygen -lf {{ output_dir }}/privatekey1 | grep -o -E '^[0-9]+'"
|
||||
register: privatekey1
|
||||
|
||||
- name: Validate privatekey1 (assert - RSA key with size 4096 bits)
|
||||
- name: Validate privatekey1 (assert - RSA key with size 2048 bits)
|
||||
assert:
|
||||
that:
|
||||
- privatekey1.stdout == '4096'
|
||||
- privatekey1.stdout == '2048'
|
||||
|
||||
- name: Validate privatekey1 idempotence
|
||||
assert:
|
||||
@@ -44,14 +52,14 @@
|
||||
- privatekey1_idem_result is not changed
|
||||
|
||||
|
||||
- name: Validate privatekey2 (test - RSA key with size 2048 bits)
|
||||
- name: Validate privatekey2 (test - RSA key with default size 4096 bits)
|
||||
shell: "ssh-keygen -lf {{ output_dir }}/privatekey2 | grep -o -E '^[0-9]+'"
|
||||
register: privatekey2
|
||||
|
||||
- name: Validate privatekey2 (assert - RSA key with size 2048 bits)
|
||||
- name: Validate privatekey2 (assert - RSA key with size 4096 bits)
|
||||
assert:
|
||||
that:
|
||||
- privatekey2.stdout == '2048'
|
||||
- privatekey2.stdout == '4096'
|
||||
|
||||
|
||||
- name: Validate privatekey3 (test - DSA key with size 1024 bits)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
dependencies:
|
||||
- setup_openssl
|
||||
- setup_pyopenssl
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
- name: Generate privatekey
|
||||
- name: "({{ select_crypto_backend }}) Generate privatekey"
|
||||
openssl_privatekey:
|
||||
path: '{{ output_dir }}/privatekey.pem'
|
||||
size: '{{ default_rsa_key_size }}'
|
||||
|
||||
- name: Generate CSR (check mode)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR (check mode)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -14,7 +15,7 @@
|
||||
check_mode: yes
|
||||
register: generate_csr_check
|
||||
|
||||
- name: Generate CSR
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -24,7 +25,7 @@
|
||||
return_content: yes
|
||||
register: generate_csr
|
||||
|
||||
- name: Generate CSR (idempotent)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR (idempotent)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -34,7 +35,7 @@
|
||||
return_content: yes
|
||||
register: generate_csr_idempotent
|
||||
|
||||
- name: Generate CSR (idempotent, check mode)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR (idempotent, check mode)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -45,7 +46,7 @@
|
||||
check_mode: yes
|
||||
register: generate_csr_idempotent_check
|
||||
|
||||
- name: Generate CSR without SAN (check mode)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR without SAN (check mode)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr-nosan.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -56,7 +57,7 @@
|
||||
check_mode: yes
|
||||
register: generate_csr_nosan_check
|
||||
|
||||
- name: Generate CSR without SAN
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR without SAN"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr-nosan.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -66,7 +67,7 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: generate_csr_nosan
|
||||
|
||||
- name: Generate CSR without SAN (idempotent)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR without SAN (idempotent)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr-nosan.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -76,7 +77,7 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: generate_csr_nosan_check_idempotent
|
||||
|
||||
- name: Generate CSR without SAN (idempotent, check mode)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR without SAN (idempotent, check mode)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr-nosan.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -91,7 +92,7 @@
|
||||
# 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: Generate CSR with KU and XKU
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with KU and XKU"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_ku_xku.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -107,7 +108,7 @@
|
||||
- biometricInfo
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
|
||||
- name: Generate CSR with KU and XKU (test idempotency)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with KU and XKU (test idempotency)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_ku_xku.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -124,7 +125,7 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: csr_ku_xku
|
||||
|
||||
- name: Generate CSR with KU and XKU (test XKU change)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with KU and XKU (test XKU change)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_ku_xku.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -140,7 +141,7 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: csr_ku_xku_change
|
||||
|
||||
- name: Generate CSR with KU and XKU (test KU change)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with KU and XKU (test KU change)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_ku_xku.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -155,14 +156,14 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: csr_ku_xku_change_2
|
||||
|
||||
- name: Generate CSR with old API
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with old API"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_oldapi.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
commonName: www.ansible.com
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
|
||||
- name: Generate CSR with invalid SAN (1/2)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with invalid SAN (1/2)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csrinvsan.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -171,7 +172,7 @@
|
||||
register: generate_csr_invalid_san
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Generate CSR with invalid SAN (2/2)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with invalid SAN (2/2)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csrinvsan2.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -180,7 +181,7 @@
|
||||
register: generate_csr_invalid_san_2
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Generate CSR with OCSP Must Staple
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with OCSP Must Staple"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_ocsp.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -188,7 +189,7 @@
|
||||
ocsp_must_staple: true
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
|
||||
- name: Generate CSR with OCSP Must Staple (test idempotency)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with OCSP Must Staple (test idempotency)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_ocsp.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -197,13 +198,13 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: csr_ocsp_idempotency
|
||||
|
||||
- name: Generate ECC privatekey
|
||||
- name: "({{ select_crypto_backend }}) Generate ECC privatekey"
|
||||
openssl_privatekey:
|
||||
path: '{{ output_dir }}/privatekey2.pem'
|
||||
type: ECC
|
||||
curve: secp384r1
|
||||
|
||||
- name: Generate CSR with ECC privatekey
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with ECC privatekey"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr2.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey2.pem'
|
||||
@@ -211,7 +212,7 @@
|
||||
commonName: www.ansible.com
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
|
||||
- name: Generate CSR with text common name
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with text common name"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr3.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey2.pem'
|
||||
@@ -220,7 +221,7 @@
|
||||
useCommonNameForSAN: no
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
|
||||
- name: Generate CSR with country name
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with country name"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr4.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey2.pem'
|
||||
@@ -228,7 +229,7 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: country_idempotent_1
|
||||
|
||||
- name: Generate CSR with country name (idempotent)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with country name (idempotent)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr4.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey2.pem'
|
||||
@@ -236,7 +237,7 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: country_idempotent_2
|
||||
|
||||
- name: Generate CSR with country name (idempotent 2)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with country name (idempotent 2)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr4.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey2.pem'
|
||||
@@ -245,7 +246,7 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: country_idempotent_3
|
||||
|
||||
- name: Generate CSR with country name (bad country name)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with country name (bad country name)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr4.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey2.pem'
|
||||
@@ -255,14 +256,15 @@
|
||||
register: country_fail_4
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Generate privatekey with password
|
||||
- name: "({{ select_crypto_backend }}) Generate privatekey with password"
|
||||
openssl_privatekey:
|
||||
path: '{{ output_dir }}/privatekeypw.pem'
|
||||
passphrase: hunter2
|
||||
cipher: auto
|
||||
select_crypto_backend: cryptography
|
||||
size: '{{ default_rsa_key_size }}'
|
||||
|
||||
- name: Generate CSR with privatekey passphrase
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with privatekey passphrase"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_pw.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekeypw.pem'
|
||||
@@ -270,7 +272,7 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: passphrase_1
|
||||
|
||||
- name: Generate CSR (failed passphrase 1)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR (failed passphrase 1)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_pw1.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -279,7 +281,7 @@
|
||||
ignore_errors: yes
|
||||
register: passphrase_error_1
|
||||
|
||||
- name: Generate CSR (failed passphrase 2)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR (failed passphrase 2)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_pw2.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekeypw.pem'
|
||||
@@ -288,7 +290,7 @@
|
||||
ignore_errors: yes
|
||||
register: passphrase_error_2
|
||||
|
||||
- name: Generate CSR (failed passphrase 3)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR (failed passphrase 3)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_pw3.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekeypw.pem'
|
||||
@@ -296,11 +298,11 @@
|
||||
ignore_errors: yes
|
||||
register: passphrase_error_3
|
||||
|
||||
- name: Create broken CSR
|
||||
- name: "({{ select_crypto_backend }}) Create broken CSR"
|
||||
copy:
|
||||
dest: "{{ output_dir }}/csrbroken.csr"
|
||||
content: "broken"
|
||||
- name: Regenerate broken CSR
|
||||
- name: "({{ select_crypto_backend }}) Regenerate broken CSR"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csrbroken.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey2.pem'
|
||||
@@ -310,7 +312,7 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: output_broken
|
||||
|
||||
- name: Generate CSR
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_backup.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -319,7 +321,7 @@
|
||||
backup: yes
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: csr_backup_1
|
||||
- name: Generate CSR (idempotent)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR (idempotent)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_backup.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -328,7 +330,7 @@
|
||||
backup: yes
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: csr_backup_2
|
||||
- name: Generate CSR (change)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR (change)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_backup.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -337,7 +339,7 @@
|
||||
backup: yes
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: csr_backup_3
|
||||
- name: Generate CSR (remove)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR (remove)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_backup.csr'
|
||||
state: absent
|
||||
@@ -345,7 +347,7 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
return_content: yes
|
||||
register: csr_backup_4
|
||||
- name: Generate CSR (remove, idempotent)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR (remove, idempotent)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_backup.csr'
|
||||
state: absent
|
||||
@@ -353,7 +355,7 @@
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: csr_backup_5
|
||||
|
||||
- name: Generate CSR with subject key identifier
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with subject key identifier"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_ski.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -364,7 +366,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: subject_key_identifier_1
|
||||
|
||||
- name: Generate CSR with subject key identifier (idempotency)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with subject key identifier (idempotency)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_ski.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -375,7 +377,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: subject_key_identifier_2
|
||||
|
||||
- name: Generate CSR with subject key identifier (change)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with subject key identifier (change)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_ski.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -386,7 +388,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: subject_key_identifier_3
|
||||
|
||||
- name: Generate CSR with subject key identifier (auto-create)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with subject key identifier (auto-create)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_ski.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -397,7 +399,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: subject_key_identifier_4
|
||||
|
||||
- name: Generate CSR with subject key identifier (auto-create idempotency)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with subject key identifier (auto-create idempotency)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_ski.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -408,7 +410,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: subject_key_identifier_5
|
||||
|
||||
- name: Generate CSR with subject key identifier (remove)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with subject key identifier (remove)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_ski.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -418,7 +420,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: subject_key_identifier_6
|
||||
|
||||
- name: Generate CSR with authority key identifier
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with authority key identifier"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_aki.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -429,7 +431,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: authority_key_identifier_1
|
||||
|
||||
- name: Generate CSR with authority key identifier (idempotency)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with authority key identifier (idempotency)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_aki.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -440,7 +442,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: authority_key_identifier_2
|
||||
|
||||
- name: Generate CSR with authority key identifier (change)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with authority key identifier (change)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_aki.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -451,7 +453,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: authority_key_identifier_3
|
||||
|
||||
- name: Generate CSR with authority key identifier (remove)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with authority key identifier (remove)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_aki.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -461,7 +463,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: authority_key_identifier_4
|
||||
|
||||
- name: Generate CSR with authority cert issuer / serial number
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with authority cert issuer / serial number"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_acisn.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -475,7 +477,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: authority_cert_issuer_sn_1
|
||||
|
||||
- name: Generate CSR with authority cert issuer / serial number (idempotency)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with authority cert issuer / serial number (idempotency)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_acisn.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -489,7 +491,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: authority_cert_issuer_sn_2
|
||||
|
||||
- name: Generate CSR with authority cert issuer / serial number (change issuer)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with authority cert issuer / serial number (change issuer)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_acisn.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -503,7 +505,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: authority_cert_issuer_sn_3
|
||||
|
||||
- name: Generate CSR with authority cert issuer / serial number (change serial number)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with authority cert issuer / serial number (change serial number)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_acisn.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -517,7 +519,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: authority_cert_issuer_sn_4
|
||||
|
||||
- name: Generate CSR with authority cert issuer / serial number (remove)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with authority cert issuer / serial number (remove)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_acisn.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -526,7 +528,7 @@
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
register: authority_cert_issuer_sn_5
|
||||
|
||||
- name: Generate CSR with everything
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with everything"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_everything.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -634,7 +636,7 @@
|
||||
- "IP:1.2.3.0/255.255.255.0"
|
||||
register: everything_1
|
||||
|
||||
- name: Generate CSR with everything (idempotent, check mode)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with everything (idempotent, check mode)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_everything.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -743,7 +745,7 @@
|
||||
check_mode: yes
|
||||
register: everything_2
|
||||
|
||||
- name: Generate CSR with everything (idempotent)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR with everything (idempotent)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_everything.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey.pem'
|
||||
@@ -851,15 +853,15 @@
|
||||
- "IP:1.2.3.0/255.255.255.0"
|
||||
register: everything_3
|
||||
|
||||
- name: Get info from CSR with everything
|
||||
- name: "({{ select_crypto_backend }}) Get info from CSR with everything"
|
||||
community.crypto.openssl_csr_info:
|
||||
path: '{{ output_dir }}/csr_everything.csr'
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: everything_info
|
||||
|
||||
- name: Ed25519 and Ed448 tests (for cryptography >= 2.6)
|
||||
- name: "({{ select_crypto_backend }}) Ed25519 and Ed448 tests (for cryptography >= 2.6)"
|
||||
block:
|
||||
- name: Generate privatekeys
|
||||
- name: "({{ select_crypto_backend }}) Generate privatekeys"
|
||||
openssl_privatekey:
|
||||
path: '{{ output_dir }}/privatekey_{{ item }}.pem'
|
||||
type: '{{ item }}'
|
||||
@@ -869,11 +871,11 @@
|
||||
register: generate_csr_ed25519_ed448_privatekey
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Generate CSR if private key generation succeeded
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR if private key generation succeeded"
|
||||
when: generate_csr_ed25519_ed448_privatekey is not failed
|
||||
block:
|
||||
|
||||
- name: Generate CSR
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_{{ item }}.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey_{{ item }}.pem'
|
||||
@@ -886,7 +888,7 @@
|
||||
register: generate_csr_ed25519_ed448
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Generate CSR (idempotent)
|
||||
- name: "({{ select_crypto_backend }}) Generate CSR (idempotent)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_{{ item }}.csr'
|
||||
privatekey_path: '{{ output_dir }}/privatekey_{{ item }}.pem'
|
||||
@@ -900,3 +902,96 @@
|
||||
ignore_errors: yes
|
||||
|
||||
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: '{{ output_dir }}/csr_crl_d_e.csr'
|
||||
privatekey_path: '{{ output_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: '{{ output_dir }}/csr_crl_d_e.csr'
|
||||
privatekey_path: '{{ output_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: '{{ output_dir }}/csr_crl_d_e.csr'
|
||||
privatekey_path: '{{ output_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_3
|
||||
|
||||
- name: "({{ select_crypto_backend }}) Create CSR with CRL distribution endpoints (no endpoints)"
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_crl_d_e.csr'
|
||||
privatekey_path: '{{ output_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: '{{ output_dir }}/csr_crl_d_e.csr'
|
||||
privatekey_path: '{{ output_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', '>=')
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
---
|
||||
####################################################################
|
||||
# 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: '{{ output_dir }}/privatekey_backend_selection.pem'
|
||||
size: '{{ default_rsa_key_size }}'
|
||||
- name: Run module with backend autodetection
|
||||
openssl_csr:
|
||||
path: '{{ output_dir }}/csr_backend_selection.csr'
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
---
|
||||
- name: Validate CSR (test - privatekey modulus)
|
||||
shell: 'openssl rsa -noout -modulus -in {{ output_dir }}/privatekey.pem'
|
||||
- name: "({{ select_crypto_backend }}) Validate CSR (test - privatekey modulus)"
|
||||
shell: '{{ openssl_binary }} rsa -noout -modulus -in {{ output_dir }}/privatekey.pem'
|
||||
register: privatekey_modulus
|
||||
|
||||
- name: Validate CSR (test - Common Name)
|
||||
shell: "openssl req -noout -subject -in {{ output_dir }}/csr.csr -nameopt oneline,-space_eq"
|
||||
- name: "({{ select_crypto_backend }}) Validate CSR (test - Common Name)"
|
||||
shell: "{{ openssl_binary }} req -noout -subject -in {{ output_dir }}/csr.csr -nameopt oneline,-space_eq"
|
||||
register: csr_cn
|
||||
|
||||
- name: Validate CSR (test - csr modulus)
|
||||
shell: 'openssl req -noout -modulus -in {{ output_dir }}/csr.csr'
|
||||
- name: "({{ select_crypto_backend }}) Validate CSR (test - csr modulus)"
|
||||
shell: '{{ openssl_binary }} req -noout -modulus -in {{ output_dir }}/csr.csr'
|
||||
register: csr_modulus
|
||||
|
||||
- name: Validate CSR (assert)
|
||||
- name: "({{ select_crypto_backend }}) Validate CSR (assert)"
|
||||
assert:
|
||||
that:
|
||||
- csr_cn.stdout.split('=')[-1] == 'www.ansible.com'
|
||||
- csr_modulus.stdout == privatekey_modulus.stdout
|
||||
|
||||
- name: Validate CSR (check mode, idempotency)
|
||||
- name: "({{ select_crypto_backend }}) Validate CSR (check mode, idempotency)"
|
||||
assert:
|
||||
that:
|
||||
- generate_csr_check is changed
|
||||
@@ -25,7 +25,7 @@
|
||||
- generate_csr_idempotent is not changed
|
||||
- generate_csr_idempotent_check is not changed
|
||||
|
||||
- name: Validate CSR (data retrieval)
|
||||
- name: "({{ select_crypto_backend }}) Validate CSR (data retrieval)"
|
||||
assert:
|
||||
that:
|
||||
- generate_csr_check.csr is none
|
||||
@@ -33,7 +33,7 @@
|
||||
- generate_csr.csr == generate_csr_idempotent.csr
|
||||
- generate_csr.csr == generate_csr_idempotent_check.csr
|
||||
|
||||
- name: Validate CSR without SAN (check mode, idempotency)
|
||||
- name: "({{ select_crypto_backend }}) Validate CSR without SAN (check mode, idempotency)"
|
||||
assert:
|
||||
that:
|
||||
- generate_csr_nosan_check is changed
|
||||
@@ -41,34 +41,34 @@
|
||||
- generate_csr_nosan_check_idempotent is not changed
|
||||
- generate_csr_nosan_check_idempotent_check is not changed
|
||||
|
||||
- name: Validate CSR_KU_XKU (assert idempotency, change)
|
||||
- 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: Validate old_API CSR (test - Common Name)
|
||||
shell: "openssl req -noout -subject -in {{ output_dir }}/csr_oldapi.csr -nameopt oneline,-space_eq"
|
||||
- name: "({{ select_crypto_backend }}) Validate old_API CSR (test - Common Name)"
|
||||
shell: "{{ openssl_binary }} req -noout -subject -in {{ output_dir }}/csr_oldapi.csr -nameopt oneline,-space_eq"
|
||||
register: csr_oldapi_cn
|
||||
|
||||
- name: Validate old_API CSR (test - csr modulus)
|
||||
shell: 'openssl req -noout -modulus -in {{ output_dir }}/csr_oldapi.csr'
|
||||
- name: "({{ select_crypto_backend }}) Validate old_API CSR (test - csr modulus)"
|
||||
shell: '{{ openssl_binary }} req -noout -modulus -in {{ output_dir }}/csr_oldapi.csr'
|
||||
register: csr_oldapi_modulus
|
||||
|
||||
- name: Validate old_API CSR (assert)
|
||||
- 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: Validate invalid SAN (1/2)
|
||||
- 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: Validate invalid SAN (2/2)
|
||||
- name: "({{ select_crypto_backend }}) Validate invalid SAN (2/2)"
|
||||
# Note that pyOpenSSL simply accepts this name, and modern cryptography versions do so as well.
|
||||
# The error has been observed with cryptography 1.7.2 and 1.9, but not with 2.3 and newer.
|
||||
assert:
|
||||
@@ -77,49 +77,49 @@
|
||||
- "'The label system:kube-controller-manager is not a valid A-label' in generate_csr_invalid_san_2.msg"
|
||||
when: select_crypto_backend == 'cryptography' and cryptography_version.stdout is version('2.0', '<')
|
||||
|
||||
- name: Validate OCSP Must Staple CSR (test - everything)
|
||||
shell: "openssl req -noout -in {{ output_dir }}/csr_ocsp.csr -text"
|
||||
- name: "({{ select_crypto_backend }}) Validate OCSP Must Staple CSR (test - everything)"
|
||||
shell: "{{ openssl_binary }} req -noout -in {{ output_dir }}/csr_ocsp.csr -text"
|
||||
register: csr_ocsp
|
||||
|
||||
- name: Validate OCSP Must Staple CSR (assert)
|
||||
- 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: Validate OCSP Must Staple CSR (assert idempotency)
|
||||
- name: "({{ select_crypto_backend }}) Validate OCSP Must Staple CSR (assert idempotency)"
|
||||
assert:
|
||||
that:
|
||||
- csr_ocsp_idempotency is not changed
|
||||
|
||||
- name: Validate ECC CSR (test - privatekey's public key)
|
||||
shell: 'openssl ec -pubout -in {{ output_dir }}/privatekey2.pem'
|
||||
- name: "({{ select_crypto_backend }}) Validate ECC CSR (test - privatekey's public key)"
|
||||
shell: '{{ openssl_binary }} ec -pubout -in {{ output_dir }}/privatekey2.pem'
|
||||
register: privatekey_ecc_key
|
||||
|
||||
- name: Validate ECC CSR (test - Common Name)
|
||||
shell: "openssl req -noout -subject -in {{ output_dir }}/csr2.csr -nameopt oneline,-space_eq"
|
||||
- name: "({{ select_crypto_backend }}) Validate ECC CSR (test - Common Name)"
|
||||
shell: "{{ openssl_binary }} req -noout -subject -in {{ output_dir }}/csr2.csr -nameopt oneline,-space_eq"
|
||||
register: csr_ecc_cn
|
||||
|
||||
- name: Validate ECC CSR (test - CSR pubkey)
|
||||
shell: 'openssl req -noout -pubkey -in {{ output_dir }}/csr2.csr'
|
||||
- name: "({{ select_crypto_backend }}) Validate ECC CSR (test - CSR pubkey)"
|
||||
shell: '{{ openssl_binary }} req -noout -pubkey -in {{ output_dir }}/csr2.csr'
|
||||
register: csr_ecc_pubkey
|
||||
|
||||
- name: Validate ECC CSR (assert)
|
||||
- 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: Validate CSR (text common name - Common Name)
|
||||
shell: "openssl req -noout -subject -in {{ output_dir }}/csr3.csr -nameopt oneline,-space_eq"
|
||||
- name: "({{ select_crypto_backend }}) Validate CSR (text common name - Common Name)"
|
||||
shell: "{{ openssl_binary }} req -noout -subject -in {{ output_dir }}/csr3.csr -nameopt oneline,-space_eq"
|
||||
register: csr3_cn
|
||||
|
||||
- name: Validate CSR (assert)
|
||||
- name: "({{ select_crypto_backend }}) Validate CSR (assert)"
|
||||
assert:
|
||||
that:
|
||||
- csr3_cn.stdout.split('=')[-1] == 'This is for Ansible'
|
||||
|
||||
- name: Validate country name idempotency and validation
|
||||
- name: "({{ select_crypto_backend }}) Validate country name idempotency and validation"
|
||||
assert:
|
||||
that:
|
||||
- country_idempotent_1 is changed
|
||||
@@ -137,12 +137,12 @@
|
||||
- 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: Verify that broken CSR will be regenerated
|
||||
- name: "({{ select_crypto_backend }}) Verify that broken CSR will be regenerated"
|
||||
assert:
|
||||
that:
|
||||
- output_broken is changed
|
||||
|
||||
- name: Verify that subject key identifier handling works
|
||||
- name: "({{ select_crypto_backend }}) Verify that subject key identifier handling works"
|
||||
assert:
|
||||
that:
|
||||
- subject_key_identifier_1 is changed
|
||||
@@ -153,7 +153,7 @@
|
||||
- subject_key_identifier_6 is changed
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
|
||||
- name: Verify that authority key identifier handling works
|
||||
- name: "({{ select_crypto_backend }}) Verify that authority key identifier handling works"
|
||||
assert:
|
||||
that:
|
||||
- authority_key_identifier_1 is changed
|
||||
@@ -162,7 +162,7 @@
|
||||
- authority_key_identifier_4 is changed
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
|
||||
- name: Verify that authority cert issuer / serial number handling works
|
||||
- name: "({{ select_crypto_backend }}) Verify that authority cert issuer / serial number handling works"
|
||||
assert:
|
||||
that:
|
||||
- authority_cert_issuer_sn_1 is changed
|
||||
@@ -172,7 +172,7 @@
|
||||
- authority_cert_issuer_sn_5 is changed
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
|
||||
- name: Check backup
|
||||
- name: "({{ select_crypto_backend }}) Check backup"
|
||||
assert:
|
||||
that:
|
||||
- csr_backup_1 is changed
|
||||
@@ -187,7 +187,7 @@
|
||||
- csr_backup_5.backup_file is undefined
|
||||
- csr_backup_4.csr is none
|
||||
|
||||
- name: Check CSR with everything
|
||||
- name: "({{ select_crypto_backend }}) Check CSR with everything"
|
||||
assert:
|
||||
that:
|
||||
- everything_1 is changed
|
||||
@@ -238,7 +238,7 @@
|
||||
]
|
||||
- everything_info.name_constraints_critical == true
|
||||
|
||||
- name: Check CSR with everything (pyOpenSSL specific)
|
||||
- name: "({{ select_crypto_backend }}) Check CSR with everything (pyOpenSSL specific)"
|
||||
assert:
|
||||
that:
|
||||
- everything_info.subject_alt_name == [
|
||||
@@ -269,7 +269,7 @@
|
||||
]
|
||||
when: select_crypto_backend == 'pyopenssl'
|
||||
|
||||
- name: Check CSR with everything (non-pyOpenSSL specific)
|
||||
- name: "({{ select_crypto_backend }}) Check CSR with everything (non-pyOpenSSL specific)"
|
||||
assert:
|
||||
that:
|
||||
- everything_info.authority_cert_issuer == [
|
||||
@@ -313,7 +313,7 @@
|
||||
]
|
||||
when: select_crypto_backend != 'pyopenssl'
|
||||
|
||||
- name: Verify Ed25519 and Ed448 tests (for cryptography >= 2.6, < 2.8)
|
||||
- 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
|
||||
@@ -324,7 +324,7 @@
|
||||
- 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: Verify Ed25519 and Ed448 tests (for cryptography >= 2.8)
|
||||
- name: "({{ select_crypto_backend }}) Verify Ed25519 and Ed448 tests (for cryptography >= 2.8)"
|
||||
assert:
|
||||
that:
|
||||
- generate_csr_ed25519_ed448 is succeeded
|
||||
@@ -334,3 +334,13 @@
|
||||
- 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', '>=')
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
dependencies:
|
||||
- setup_openssl
|
||||
- setup_pyopenssl
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
- debug:
|
||||
msg: "Executing tests with backend {{ select_crypto_backend }}"
|
||||
|
||||
- name: ({{select_crypto_backend}}) Get CSR info
|
||||
- name: "({{ select_crypto_backend }}) Get CSR info"
|
||||
openssl_csr_info:
|
||||
path: '{{ output_dir }}/csr_1.csr'
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: result
|
||||
|
||||
- name: Check whether subject behaves as expected
|
||||
- name: "({{ select_crypto_backend }}) Check whether subject behaves as expected"
|
||||
assert:
|
||||
that:
|
||||
- result.subject.organizationalUnitName == 'ACME Department'
|
||||
- "['organizationalUnitName', 'Crypto Department'] in result.subject_ordered"
|
||||
- "['organizationalUnitName', 'ACME Department'] in result.subject_ordered"
|
||||
|
||||
- name: Check SubjectKeyIdentifier and AuthorityKeyIdentifier
|
||||
- name: "({{ select_crypto_backend }}) Check SubjectKeyIdentifier and AuthorityKeyIdentifier"
|
||||
assert:
|
||||
that:
|
||||
- result.subject_key_identifier == "00:11:22:33"
|
||||
@@ -28,38 +28,38 @@
|
||||
- "IP:1.2.3.4"
|
||||
when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=')
|
||||
|
||||
- name: Update result list
|
||||
- name: "({{ select_crypto_backend }}) Update result list"
|
||||
set_fact:
|
||||
info_results: "{{ info_results + [result] }}"
|
||||
|
||||
- name: ({{select_crypto_backend}}) Get CSR info directly
|
||||
- name: "({{ select_crypto_backend }}) Get CSR info directly"
|
||||
openssl_csr_info:
|
||||
content: '{{ lookup("file", output_dir ~ "/csr_1.csr") }}'
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: result_direct
|
||||
|
||||
- name: ({{select_crypto_backend}}) Compare output of direct and loaded info
|
||||
- name: "({{ select_crypto_backend }}) Compare output of direct and loaded info"
|
||||
assert:
|
||||
that:
|
||||
- result == result_direct
|
||||
|
||||
- name: ({{select_crypto_backend}}) Get CSR info
|
||||
- name: "({{ select_crypto_backend }}) Get CSR info"
|
||||
openssl_csr_info:
|
||||
path: '{{ output_dir }}/csr_2.csr'
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: result
|
||||
|
||||
- name: Update result list
|
||||
- name: "({{ select_crypto_backend }}) Update result list"
|
||||
set_fact:
|
||||
info_results: "{{ info_results + [result] }}"
|
||||
|
||||
- name: ({{select_crypto_backend}}) Get CSR info
|
||||
- name: "({{ select_crypto_backend }}) Get CSR info"
|
||||
openssl_csr_info:
|
||||
path: '{{ output_dir }}/csr_3.csr'
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: result
|
||||
|
||||
- name: Check AuthorityKeyIdentifier
|
||||
- name: "({{ select_crypto_backend }}) Check AuthorityKeyIdentifier"
|
||||
assert:
|
||||
that:
|
||||
- result.authority_key_identifier is none
|
||||
@@ -71,17 +71,17 @@
|
||||
- "IP:1.2.3.4"
|
||||
when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=')
|
||||
|
||||
- name: Update result list
|
||||
- name: "({{ select_crypto_backend }}) Update result list"
|
||||
set_fact:
|
||||
info_results: "{{ info_results + [result] }}"
|
||||
|
||||
- name: ({{select_crypto_backend}}) Get CSR info
|
||||
- name: "({{ select_crypto_backend }}) Get CSR info"
|
||||
openssl_csr_info:
|
||||
path: '{{ output_dir }}/csr_4.csr'
|
||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||
register: result
|
||||
|
||||
- name: Check AuthorityKeyIdentifier
|
||||
- name: "({{ select_crypto_backend }}) Check AuthorityKeyIdentifier"
|
||||
assert:
|
||||
that:
|
||||
- result.authority_key_identifier == "44:55:66:77"
|
||||
@@ -89,6 +89,6 @@
|
||||
- result.authority_cert_serial_number is none
|
||||
when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=')
|
||||
|
||||
- name: Update result list
|
||||
- name: "({{ select_crypto_backend }}) Update result list"
|
||||
set_fact:
|
||||
info_results: "{{ info_results + [result] }}"
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
---
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
- name: Generate privatekey
|
||||
openssl_privatekey:
|
||||
path: '{{ output_dir }}/privatekey.pem'
|
||||
size: '{{ default_rsa_key_size }}'
|
||||
|
||||
- name: Generate privatekey with password
|
||||
openssl_privatekey:
|
||||
@@ -9,6 +15,7 @@
|
||||
passphrase: hunter2
|
||||
cipher: auto
|
||||
select_crypto_backend: cryptography
|
||||
size: '{{ default_rsa_key_size }}'
|
||||
|
||||
- name: Generate CSR 1
|
||||
openssl_csr:
|
||||
|
||||
2
tests/integration/targets/openssl_csr_pipe/aliases
Normal file
2
tests/integration/targets/openssl_csr_pipe/aliases
Normal file
@@ -0,0 +1,2 @@
|
||||
shippable/posix/group1
|
||||
destructive
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user