mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4834c0cb4b | ||
|
|
1f0016c616 | ||
|
|
74e4be139f | ||
|
|
b584336b62 | ||
|
|
20b0d7a298 | ||
|
|
5741f24aa9 | ||
|
|
b5dfc9fc75 | ||
|
|
4318e95618 | ||
|
|
113cbb6eb8 | ||
|
|
3ebed060b7 | ||
|
|
270ef0db47 | ||
|
|
0b306b436a | ||
|
|
c5519bc557 | ||
|
|
23d6f2ae75 | ||
|
|
9da7c54ae9 | ||
|
|
928cb3aa9b | ||
|
|
3e6815d73f | ||
|
|
cb08f56066 | ||
|
|
e3f486a063 | ||
|
|
0a1e25e16a | ||
|
|
ff4966ad3f | ||
|
|
0cb10be2d5 | ||
|
|
901863989b | ||
|
|
99377764c1 | ||
|
|
426d70fbcf | ||
|
|
f315722b31 | ||
|
|
73afe8e742 | ||
|
|
db67b8a857 | ||
|
|
e05475d58a | ||
|
|
dceee8f50e | ||
|
|
b893252ad1 | ||
|
|
0755a2b657 | ||
|
|
90bf8b0b2e | ||
|
|
5ff28c751d | ||
|
|
7b08edb5a4 | ||
|
|
fbadcbeb29 | ||
|
|
e991375f55 | ||
|
|
33c99014ae | ||
|
|
fbd6ff6ead | ||
|
|
c4ab2eb3b5 | ||
|
|
44b6df0ce5 | ||
|
|
14a42505a9 | ||
|
|
44cbd33cb7 | ||
|
|
4411a71d06 | ||
|
|
bfe37bc668 | ||
|
|
d784e0a52b | ||
|
|
d73a2942a2 | ||
|
|
8af4847373 | ||
|
|
44f7367e21 | ||
|
|
0733b0d521 | ||
|
|
771a9eebcf | ||
|
|
0fdede5d7a | ||
|
|
56b2130c6e | ||
|
|
6c018b94da | ||
|
|
63f4598737 | ||
|
|
598cdf0a21 | ||
|
|
eea7bfc6bf | ||
|
|
8521c96e8a | ||
|
|
d90cc5142b | ||
|
|
37aab65396 | ||
|
|
baff003ea8 | ||
|
|
03427e35a7 | ||
|
|
170fa40014 | ||
|
|
330b30d5d2 | ||
|
|
67b8274faf | ||
|
|
02ee3fb974 | ||
|
|
93ced1956c | ||
|
|
a9e358ea57 | ||
|
|
ffcdbc5d0c | ||
|
|
6740cae10f | ||
|
|
915379459d | ||
|
|
a4a12bae27 | ||
|
|
94fc356338 | ||
|
|
08ada24a53 | ||
|
|
b59846b9fa | ||
|
|
c9ec463893 | ||
|
|
38ce150f80 |
@@ -19,6 +19,11 @@ schedules:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- cron: 0 12 * * 0
|
||||
displayName: Weekly (old stable branches)
|
||||
always: true
|
||||
branches:
|
||||
include:
|
||||
- stable-*
|
||||
|
||||
variables:
|
||||
@@ -55,6 +60,17 @@ stages:
|
||||
test: 'devel/sanity/extra'
|
||||
- name: Units
|
||||
test: 'devel/units/1'
|
||||
- stage: Ansible_2_12
|
||||
displayName: Sanity & Units 2.12
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
targets:
|
||||
- name: Sanity
|
||||
test: '2.12/sanity/1'
|
||||
- name: Units
|
||||
test: '2.12/units/1'
|
||||
- stage: Ansible_2_11
|
||||
displayName: Sanity & Units 2.11
|
||||
dependsOn: []
|
||||
@@ -97,16 +113,12 @@ stages:
|
||||
parameters:
|
||||
testFormat: devel/linux/{0}/1
|
||||
targets:
|
||||
- name: CentOS 6
|
||||
test: centos6
|
||||
- name: CentOS 7
|
||||
test: centos7
|
||||
- name: CentOS 8
|
||||
test: centos8
|
||||
- name: Fedora 33
|
||||
test: fedora33
|
||||
- name: Fedora 34
|
||||
test: fedora34
|
||||
- name: Fedora 35
|
||||
test: fedora35
|
||||
- name: openSUSE 15 py2
|
||||
test: opensuse15py2
|
||||
- name: openSUSE 15 py3
|
||||
@@ -115,6 +127,24 @@ stages:
|
||||
test: ubuntu1804
|
||||
- name: Ubuntu 20.04
|
||||
test: ubuntu2004
|
||||
- stage: Docker_2_12
|
||||
displayName: Docker 2.12
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: 2.12/linux/{0}/1
|
||||
targets:
|
||||
- name: CentOS 6
|
||||
test: centos6
|
||||
- name: CentOS 8
|
||||
test: centos8
|
||||
- name: Fedora 33
|
||||
test: fedora33
|
||||
- name: openSUSE 15 py3
|
||||
test: opensuse15
|
||||
- name: Ubuntu 20.04
|
||||
test: ubuntu2004
|
||||
- stage: Docker_2_11
|
||||
displayName: Docker 2.11
|
||||
dependsOn: []
|
||||
@@ -131,12 +161,8 @@ stages:
|
||||
test: fedora32
|
||||
- name: openSUSE 15 py2
|
||||
test: opensuse15py2
|
||||
- name: openSUSE 15 py3
|
||||
test: opensuse15
|
||||
- name: Ubuntu 18.04
|
||||
test: ubuntu1804
|
||||
- name: Ubuntu 20.04
|
||||
test: ubuntu2004
|
||||
- stage: Docker_2_10
|
||||
displayName: Docker 2.10
|
||||
dependsOn: []
|
||||
@@ -147,22 +173,10 @@ stages:
|
||||
targets:
|
||||
- name: CentOS 6
|
||||
test: centos6
|
||||
- name: CentOS 7
|
||||
test: centos7
|
||||
- name: CentOS 8
|
||||
test: centos8
|
||||
- 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: []
|
||||
@@ -175,16 +189,8 @@ stages:
|
||||
test: centos6
|
||||
- name: CentOS 7
|
||||
test: centos7
|
||||
- name: CentOS 8
|
||||
test: centos8
|
||||
- name: Fedora 30
|
||||
test: fedora30
|
||||
- name: Fedora 31
|
||||
test: fedora31
|
||||
- name: openSUSE 15 py2
|
||||
test: opensuse15py2
|
||||
- name: openSUSE 15 py3
|
||||
test: opensuse15
|
||||
- name: Ubuntu 16.04
|
||||
test: ubuntu1604
|
||||
- name: Ubuntu 18.04
|
||||
@@ -203,12 +209,26 @@ stages:
|
||||
test: macos/11.1
|
||||
- name: RHEL 7.9
|
||||
test: rhel/7.9
|
||||
- name: RHEL 8.4
|
||||
test: rhel/8.4
|
||||
- name: RHEL 8.5
|
||||
test: rhel/8.5
|
||||
- name: FreeBSD 12.2
|
||||
test: freebsd/12.2
|
||||
- name: FreeBSD 13.0
|
||||
test: freebsd/13.0
|
||||
- stage: Remote_2_12
|
||||
displayName: Remote 2.12
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: 2.12/{0}/1
|
||||
targets:
|
||||
- name: macOS 11.1
|
||||
test: macos/11.1
|
||||
- name: RHEL 8.4
|
||||
test: rhel/8.4
|
||||
- name: FreeBSD 13.0
|
||||
test: freebsd/13.0
|
||||
- stage: Remote_2_11
|
||||
displayName: Remote 2.11
|
||||
dependsOn: []
|
||||
@@ -221,8 +241,6 @@ stages:
|
||||
test: rhel/7.9
|
||||
- name: RHEL 8.3
|
||||
test: rhel/8.3
|
||||
- name: macOS 11.1
|
||||
test: macos/11.1
|
||||
- name: FreeBSD 12.2
|
||||
test: freebsd/12.2
|
||||
- stage: Remote_2_10
|
||||
@@ -233,8 +251,6 @@ stages:
|
||||
parameters:
|
||||
testFormat: 2.10/{0}/1
|
||||
targets:
|
||||
- name: RHEL 7.8
|
||||
test: rhel/7.8
|
||||
- name: OS X 10.11
|
||||
test: osx/10.11
|
||||
- name: macOS 10.15
|
||||
@@ -261,7 +277,6 @@ stages:
|
||||
nameFormat: Python {0}
|
||||
testFormat: devel/cloud/{0}/1
|
||||
targets:
|
||||
- test: 2.6
|
||||
- test: 2.7
|
||||
- test: 3.5
|
||||
- test: 3.6
|
||||
@@ -269,6 +284,17 @@ stages:
|
||||
- test: 3.8
|
||||
- test: 3.9
|
||||
- test: "3.10"
|
||||
- stage: Cloud_2_12
|
||||
displayName: Cloud 2.12
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
nameFormat: Python {0}
|
||||
testFormat: 2.12/cloud/{0}/1
|
||||
targets:
|
||||
- test: 2.6
|
||||
- test: 3.9
|
||||
- stage: Cloud_2_11
|
||||
displayName: Cloud 2.11
|
||||
dependsOn: []
|
||||
@@ -306,20 +332,24 @@ stages:
|
||||
condition: succeededOrFailed()
|
||||
dependsOn:
|
||||
- Ansible_devel
|
||||
- Ansible_2_12
|
||||
- Ansible_2_11
|
||||
- Ansible_2_10
|
||||
- Ansible_2_9
|
||||
- Remote_devel
|
||||
- Docker_devel
|
||||
- Cloud_devel
|
||||
- Remote_2_12
|
||||
- Remote_2_11
|
||||
- Docker_2_11
|
||||
- Cloud_2_11
|
||||
- Remote_2_10
|
||||
- Docker_2_10
|
||||
- Cloud_2_10
|
||||
- Remote_2_9
|
||||
- Docker_devel
|
||||
- Docker_2_12
|
||||
- Docker_2_11
|
||||
- Docker_2_10
|
||||
- Docker_2_9
|
||||
- Cloud_devel
|
||||
- Cloud_2_12
|
||||
- Cloud_2_11
|
||||
- Cloud_2_10
|
||||
- Cloud_2_9
|
||||
jobs:
|
||||
- template: templates/coverage.yml
|
||||
|
||||
@@ -11,7 +11,7 @@ mkdir "${agent_temp_directory}/coverage/"
|
||||
|
||||
options=(--venv --venv-system-site-packages --color -v)
|
||||
|
||||
ansible-test coverage combine --export "${agent_temp_directory}/coverage/" "${options[@]}"
|
||||
ansible-test coverage combine --group-by command --export "${agent_temp_directory}/coverage/" "${options[@]}"
|
||||
|
||||
if ansible-test coverage analyze targets generate --help >/dev/null 2>&1; then
|
||||
# Only analyze coverage if the installed version of ansible-test supports it.
|
||||
|
||||
101
.azure-pipelines/scripts/publish-codecov.py
Executable file
101
.azure-pipelines/scripts/publish-codecov.py
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Upload code coverage reports to codecov.io.
|
||||
Multiple coverage files from multiple languages are accepted and aggregated after upload.
|
||||
Python coverage, as well as PowerShell and Python stubs can all be uploaded.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import typing as t
|
||||
import urllib.request
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CoverageFile:
|
||||
name: str
|
||||
path: pathlib.Path
|
||||
flags: t.List[str]
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Args:
|
||||
dry_run: bool
|
||||
path: pathlib.Path
|
||||
|
||||
|
||||
def parse_args() -> Args:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-n', '--dry-run', action='store_true')
|
||||
parser.add_argument('path', type=pathlib.Path)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Store arguments in a typed dataclass
|
||||
fields = dataclasses.fields(Args)
|
||||
kwargs = {field.name: getattr(args, field.name) for field in fields}
|
||||
|
||||
return Args(**kwargs)
|
||||
|
||||
|
||||
def process_files(directory: pathlib.Path) -> t.Tuple[CoverageFile, ...]:
|
||||
processed = []
|
||||
for file in directory.joinpath('reports').glob('coverage*.xml'):
|
||||
name = file.stem.replace('coverage=', '')
|
||||
|
||||
# Get flags from name
|
||||
flags = name.replace('-powershell', '').split('=') # Drop '-powershell' suffix
|
||||
flags = [flag if not flag.startswith('stub') else flag.split('-')[0] for flag in flags] # Remove "-01" from stub files
|
||||
|
||||
processed.append(CoverageFile(name, file, flags))
|
||||
|
||||
return tuple(processed)
|
||||
|
||||
|
||||
def upload_files(codecov_bin: pathlib.Path, files: t.Tuple[CoverageFile, ...], dry_run: bool = False) -> None:
|
||||
for file in files:
|
||||
cmd = [
|
||||
str(codecov_bin),
|
||||
'--name', file.name,
|
||||
'--file', str(file.path),
|
||||
]
|
||||
for flag in file.flags:
|
||||
cmd.extend(['--flags', flag])
|
||||
|
||||
if dry_run:
|
||||
print(f'DRY-RUN: Would run command: {cmd}')
|
||||
continue
|
||||
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
|
||||
def download_file(url: str, dest: pathlib.Path, flags: int, dry_run: bool = False) -> None:
|
||||
if dry_run:
|
||||
print(f'DRY-RUN: Would download {url} to {dest} and set mode to {flags:o}')
|
||||
return
|
||||
|
||||
with urllib.request.urlopen(url) as resp:
|
||||
with dest.open('w+b') as f:
|
||||
# Read data in chunks rather than all at once
|
||||
shutil.copyfileobj(resp, f, 64 * 1024)
|
||||
|
||||
dest.chmod(flags)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
url = 'https://ansible-ci-files.s3.amazonaws.com/codecov/linux/codecov'
|
||||
with tempfile.TemporaryDirectory(prefix='codecov-') as tmpdir:
|
||||
codecov_bin = pathlib.Path(tmpdir) / 'codecov'
|
||||
download_file(url, codecov_bin, 0o755, args.dry_run)
|
||||
|
||||
files = process_files(args.path)
|
||||
upload_files(codecov_bin, files, args.dry_run)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/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://ansible-ci-files.s3.us-east-1.amazonaws.com/codecov/codecov.sh > 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
|
||||
@@ -12,4 +12,4 @@ if ! ansible-test --help >/dev/null 2>&1; then
|
||||
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
|
||||
ansible-test coverage xml --group-by command --stub --venv --venv-system-site-packages --color -v
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
summaryFileLocation: "$(outputPath)/reports/$(pipelinesCoverage).xml"
|
||||
displayName: Publish to Azure Pipelines
|
||||
condition: gt(variables.coverageFileCount, 0)
|
||||
- bash: .azure-pipelines/scripts/publish-codecov.sh "$(outputPath)"
|
||||
- bash: .azure-pipelines/scripts/publish-codecov.py "$(outputPath)"
|
||||
displayName: Publish to codecov.io
|
||||
condition: gt(variables.coverageFileCount, 0)
|
||||
continueOnError: true
|
||||
|
||||
135
CHANGELOG.rst
135
CHANGELOG.rst
@@ -5,6 +5,141 @@ Community Crypto Release Notes
|
||||
.. contents:: Topics
|
||||
|
||||
|
||||
v1.9.9
|
||||
======
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Various modules and plugins - use vendored version of ``distutils.version`` instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.crypto/pull/353).
|
||||
- certificate_complete_chain - do not append root twice if the chain already ends with a root certificate (https://github.com/ansible-collections/community.crypto/pull/360).
|
||||
- certificate_complete_chain - do not hang when infinite loop is found (https://github.com/ansible-collections/community.crypto/issues/355, https://github.com/ansible-collections/community.crypto/pull/360).
|
||||
|
||||
v1.9.8
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Documentation fix release. No actual code changes.
|
||||
|
||||
v1.9.7
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release with extra forward compatibility for newer versions of cryptography.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- acme_* modules - fix usage of ``fetch_url`` with changes in latest ansible-core ``devel`` branch (https://github.com/ansible-collections/community.crypto/pull/339).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- acme_certificate - avoid passing multiple certificates to ``cryptography``'s X.509 certificate loader when ``fullchain_dest`` is used (https://github.com/ansible-collections/community.crypto/pull/324).
|
||||
- get_certificate, openssl_csr_info, x509_certificate_info - add fallback code for extension parsing that works with cryptography 36.0.0 and newer. This code re-serializes de-serialized extensions and thus can return slightly different values if the extension in the original CSR resp. certificate was not canonicalized correctly. This code is currently used as a fallback if the existing code stops working, but we will switch it to be the main code in a future release (https://github.com/ansible-collections/community.crypto/pull/331).
|
||||
- luks_device - now also runs a built-in LUKS signature cleaner on ``state=absent`` to make sure that also the secondary LUKS2 header is wiped when older versions of wipefs are used (https://github.com/ansible-collections/community.crypto/issues/326, https://github.com/ansible-collections/community.crypto/pull/327).
|
||||
- openssl_pkcs12 - use new PKCS#12 deserialization infrastructure from cryptography 36.0.0 if available (https://github.com/ansible-collections/community.crypto/pull/302).
|
||||
|
||||
v1.9.6
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- cryptography backend - improve Unicode handling for Python 2 (https://github.com/ansible-collections/community.crypto/pull/313).
|
||||
|
||||
v1.9.5
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release to fully support cryptography 35.0.0.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- get_certificate - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release in PyOpenSSL backend (https://github.com/ansible-collections/community.crypto/pull/300).
|
||||
- openssl_pkcs12 - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/296).
|
||||
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release in PyOpenSSL backend (https://github.com/ansible-collections/community.crypto/pull/300).
|
||||
|
||||
v1.9.4
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- acme_* modules - fix commands composed for OpenSSL backend to retrieve information on CSRs and certificates from stdin to use ``/dev/stdin`` instead of ``-``. This is needed for OpenSSL 1.0.1 and 1.0.2, apparently (https://github.com/ansible-collections/community.crypto/pull/279).
|
||||
- acme_challenge_cert_helper - only return exception when cryptography is not installed, not when a too old version of it is installed. This prevents Ansible's callback to crash (https://github.com/ansible-collections/community.crypto/pull/281).
|
||||
|
||||
v1.9.3
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssl_csr and openssl_csr_pipe - make sure that Unicode strings are used to compare strings with the cryptography backend. This fixes idempotency problems with non-ASCII letters on Python 2 (https://github.com/ansible-collections/community.crypto/issues/270, https://github.com/ansible-collections/community.crypto/pull/271).
|
||||
|
||||
v1.9.2
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release to fix the changelog. No other change compared to 1.9.0.
|
||||
|
||||
v1.9.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Accidental 1.9.1 release. Identical to 1.9.0.
|
||||
|
||||
v1.9.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular feature release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- get_certificate - added ``starttls`` option to retrieve certificates from servers which require clients to request an encrypted connection (https://github.com/ansible-collections/community.crypto/pull/264).
|
||||
- openssh_keypair - added ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- keypair_backend module utils - simplify code to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
- openssh_keypair - fixed ``cryptography`` backend to preserve original file permissions when regenerating a keypair requires existing files to be overwritten (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- openssh_keypair - fixed error handling to restore original keypair if regeneration fails (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- x509_crl - restore inherited function signature to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
|
||||
v1.8.0
|
||||
======
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Please note that this collection does **not** support Windows targets.
|
||||
|
||||
## Tested with Ansible
|
||||
|
||||
Tested with the current Ansible 2.9, ansible-base 2.10 and ansible-core 2.11 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
|
||||
Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11 and ansible-core 2.12 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
|
||||
|
||||
## External requirements
|
||||
|
||||
|
||||
@@ -503,3 +503,141 @@ releases:
|
||||
- 257-openssh-keypair-fix-pubkey-permissions.yml
|
||||
- ansible-core-_text.yml
|
||||
release_date: '2021-08-10'
|
||||
1.9.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- keypair_backend module utils - simplify code to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
- openssh_keypair - fixed ``cryptography`` backend to preserve original file
|
||||
permissions when regenerating a keypair requires existing files to be overwritten
|
||||
(https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- openssh_keypair - fixed error handling to restore original keypair if regeneration
|
||||
fails (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- x509_crl - restore inherited function signature to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
minor_changes:
|
||||
- get_certificate - added ``starttls`` option to retrieve certificates from
|
||||
servers which require clients to request an encrypted connection (https://github.com/ansible-collections/community.crypto/pull/264).
|
||||
- openssh_keypair - added ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
release_summary: Regular feature release.
|
||||
fragments:
|
||||
- 1.9.0.yml
|
||||
- 260-openssh_keypair-diff-support.yml
|
||||
- 263-sanity.yml
|
||||
- 264-get_certificate-add-starttls-option.yml
|
||||
release_date: '2021-08-30'
|
||||
1.9.1:
|
||||
changes:
|
||||
release_summary: Accidental 1.9.1 release. Identical to 1.9.0.
|
||||
release_date: '2021-08-30'
|
||||
1.9.2:
|
||||
changes:
|
||||
release_summary: Bugfix release to fix the changelog. No other change compared
|
||||
to 1.9.0.
|
||||
fragments:
|
||||
- 1.9.2.yml
|
||||
release_date: '2021-08-30'
|
||||
1.9.3:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssl_csr and openssl_csr_pipe - make sure that Unicode strings are used
|
||||
to compare strings with the cryptography backend. This fixes idempotency problems
|
||||
with non-ASCII letters on Python 2 (https://github.com/ansible-collections/community.crypto/issues/270,
|
||||
https://github.com/ansible-collections/community.crypto/pull/271).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.3.yml
|
||||
- 271-openssl_csr-utf8.yml
|
||||
release_date: '2021-09-14'
|
||||
1.9.4:
|
||||
changes:
|
||||
bugfixes:
|
||||
- acme_* modules - fix commands composed for OpenSSL backend to retrieve information
|
||||
on CSRs and certificates from stdin to use ``/dev/stdin`` instead of ``-``.
|
||||
This is needed for OpenSSL 1.0.1 and 1.0.2, apparently (https://github.com/ansible-collections/community.crypto/pull/279).
|
||||
- acme_challenge_cert_helper - only return exception when cryptography is not
|
||||
installed, not when a too old version of it is installed. This prevents Ansible's
|
||||
callback to crash (https://github.com/ansible-collections/community.crypto/pull/281).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.4.yml
|
||||
- 279-acme-openssl.yml
|
||||
- 282-acme_challenge_cert_helper-error.yml
|
||||
release_date: '2021-09-28'
|
||||
1.9.5:
|
||||
changes:
|
||||
bugfixes:
|
||||
- get_certificate - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release
|
||||
(https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release
|
||||
in PyOpenSSL backend (https://github.com/ansible-collections/community.crypto/pull/300).
|
||||
- openssl_pkcs12 - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/296).
|
||||
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release
|
||||
(https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release
|
||||
in PyOpenSSL backend (https://github.com/ansible-collections/community.crypto/pull/300).
|
||||
release_summary: Bugfix release to fully support cryptography 35.0.0.
|
||||
fragments:
|
||||
- 1.9.5.yml
|
||||
- 294-cryptography-35.0.0.yml
|
||||
- 296-openssl_pkcs12-cryptography-35.yml
|
||||
- 300-pyopenssl-cryptography-35.yml
|
||||
release_date: '2021-10-06'
|
||||
1.9.6:
|
||||
changes:
|
||||
bugfixes:
|
||||
- cryptography backend - improve Unicode handling for Python 2 (https://github.com/ansible-collections/community.crypto/pull/313).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.6.yml
|
||||
- 313-unicode-names.yml
|
||||
release_date: '2021-10-30'
|
||||
1.9.7:
|
||||
changes:
|
||||
bugfixes:
|
||||
- acme_certificate - avoid passing multiple certificates to ``cryptography``'s
|
||||
X.509 certificate loader when ``fullchain_dest`` is used (https://github.com/ansible-collections/community.crypto/pull/324).
|
||||
- get_certificate, openssl_csr_info, x509_certificate_info - add fallback code
|
||||
for extension parsing that works with cryptography 36.0.0 and newer. This
|
||||
code re-serializes de-serialized extensions and thus can return slightly different
|
||||
values if the extension in the original CSR resp. certificate was not canonicalized
|
||||
correctly. This code is currently used as a fallback if the existing code
|
||||
stops working, but we will switch it to be the main code in a future release
|
||||
(https://github.com/ansible-collections/community.crypto/pull/331).
|
||||
- luks_device - now also runs a built-in LUKS signature cleaner on ``state=absent``
|
||||
to make sure that also the secondary LUKS2 header is wiped when older versions
|
||||
of wipefs are used (https://github.com/ansible-collections/community.crypto/issues/326,
|
||||
https://github.com/ansible-collections/community.crypto/pull/327).
|
||||
- openssl_pkcs12 - use new PKCS#12 deserialization infrastructure from cryptography
|
||||
36.0.0 if available (https://github.com/ansible-collections/community.crypto/pull/302).
|
||||
minor_changes:
|
||||
- acme_* modules - fix usage of ``fetch_url`` with changes in latest ansible-core
|
||||
``devel`` branch (https://github.com/ansible-collections/community.crypto/pull/339).
|
||||
release_summary: Bugfix release with extra forward compatibility for newer versions
|
||||
of cryptography.
|
||||
fragments:
|
||||
- 1.9.7.yml
|
||||
- 302-openssl_pkcs12-cryptography-36.0.0.yml
|
||||
- 324-acme_certificate-fullchain.yml
|
||||
- 327-luks_device-wipe.yml
|
||||
- 331-cryptography-extensions.yml
|
||||
- fetch_url-devel.yml
|
||||
release_date: '2021-11-22'
|
||||
1.9.8:
|
||||
changes:
|
||||
release_summary: Documentation fix release. No actual code changes.
|
||||
fragments:
|
||||
- 1.9.8.yml
|
||||
release_date: '2021-12-13'
|
||||
1.9.9:
|
||||
changes:
|
||||
bugfixes:
|
||||
- Various modules and plugins - use vendored version of ``distutils.version``
|
||||
instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.crypto/pull/353).
|
||||
- certificate_complete_chain - do not append root twice if the chain already
|
||||
ends with a root certificate (https://github.com/ansible-collections/community.crypto/pull/360).
|
||||
- certificate_complete_chain - do not hang when infinite loop is found (https://github.com/ansible-collections/community.crypto/issues/355,
|
||||
https://github.com/ansible-collections/community.crypto/pull/360).
|
||||
fragments:
|
||||
- 353-distutils.version.yml
|
||||
- 360-certificate_complete_chain-loop.yml
|
||||
release_date: '2022-01-11'
|
||||
|
||||
@@ -71,7 +71,7 @@ In the following example, we assume that the certificate to sign (including its
|
||||
|
||||
- name: Sign certificate with our CA
|
||||
community.crypto.x509_certificate_pipe:
|
||||
csr_content: "{{ ca_csr.csr }}"
|
||||
csr_content: "{{ csr.csr }}"
|
||||
provider: ownca
|
||||
ownca_path: /path/to/ca-certificate.pem
|
||||
ownca_privatekey_path: /path/to/ca-certificate.key
|
||||
@@ -128,7 +128,7 @@ Please note that the above procedure is **not idempotent**. The following extend
|
||||
- name: Sign certificate with our CA
|
||||
community.crypto.x509_certificate_pipe:
|
||||
content: "{{ (certificate.content | b64decode) if certificate_exists.stat.exists else omit }}"
|
||||
csr_content: "{{ ca_csr.csr }}"
|
||||
csr_content: "{{ csr.csr }}"
|
||||
provider: ownca
|
||||
ownca_path: /path/to/ca-certificate.pem
|
||||
ownca_privatekey_path: /path/to/ca-certificate.key
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace: community
|
||||
name: crypto
|
||||
version: 1.8.0
|
||||
version: 1.9.9
|
||||
readme: README.md
|
||||
authors:
|
||||
- Ansible (github.com/ansible)
|
||||
|
||||
@@ -457,8 +457,8 @@ options:
|
||||
- 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.
|
||||
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||
- This is only used by the C(ownca) provider.
|
||||
type: str
|
||||
default: +0s
|
||||
@@ -470,8 +470,8 @@ options:
|
||||
- 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.
|
||||
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||
- 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.
|
||||
@@ -548,8 +548,8 @@ options:
|
||||
- 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.
|
||||
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||
- This is only used by the C(selfsigned) provider.
|
||||
type: str
|
||||
default: +0s
|
||||
@@ -562,8 +562,8 @@ options:
|
||||
- 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.
|
||||
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||
- 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.
|
||||
|
||||
@@ -227,12 +227,11 @@ options:
|
||||
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),
|
||||
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier),
|
||||
I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
|
||||
type: str
|
||||
authority_cert_issuer:
|
||||
@@ -241,23 +240,24 @@ options:
|
||||
- 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.
|
||||
- If specified, I(authority_cert_serial_number) must also be specified.
|
||||
- "Please note that commercial CAs ignore this value, respectively use a value of their
|
||||
own choice. Specifying this option is mostly useful for self-signed certificates
|
||||
or for own CAs."
|
||||
- Note that this is only supported if the C(cryptography) backend is used!
|
||||
- The C(AuthorityKeyIdentifier) will only be added if at least one of I(authority_key_identifier),
|
||||
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier),
|
||||
I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
|
||||
type: list
|
||||
elements: str
|
||||
authority_cert_serial_number:
|
||||
description:
|
||||
- The authority cert serial number.
|
||||
- If specified, I(authority_cert_issuer) must also be specified.
|
||||
- Note that this is only supported if the C(cryptography) backend is used!
|
||||
- "Please note that commercial CAs ignore this value, respectively use a value of their
|
||||
own choice. Specifying this option is mostly useful for self-signed certificates
|
||||
or for own CAs."
|
||||
- The C(AuthorityKeyIdentifier) will only be added if at least one of I(authority_key_identifier),
|
||||
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier),
|
||||
I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
|
||||
type: int
|
||||
crl_distribution_points:
|
||||
|
||||
@@ -100,7 +100,7 @@ options:
|
||||
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,
|
||||
- The value C(auto) selects a format based on the key format. The value C(auto_ignore) does the same,
|
||||
but for existing private key files, it will not force a regenerate when its format is not the automatically
|
||||
selected one for generation.
|
||||
- Note that if the format for an existing private key mismatches, the key is B(regenerated) by default.
|
||||
|
||||
343
plugins/module_utils/_version.py
Normal file
343
plugins/module_utils/_version.py
Normal file
@@ -0,0 +1,343 @@
|
||||
# Vendored copy of distutils/version.py from CPython 3.9.5
|
||||
#
|
||||
# Implements multiple version numbering conventions for the
|
||||
# Python Module Distribution Utilities.
|
||||
#
|
||||
# PSF License (see licenses/PSF-license.txt or https://opensource.org/licenses/Python-2.0)
|
||||
#
|
||||
|
||||
"""Provides classes to represent module version numbers (one class for
|
||||
each style of version numbering). There are currently two such classes
|
||||
implemented: StrictVersion and LooseVersion.
|
||||
|
||||
Every version number class implements the following interface:
|
||||
* the 'parse' method takes a string and parses it to some internal
|
||||
representation; if the string is an invalid version number,
|
||||
'parse' raises a ValueError exception
|
||||
* the class constructor takes an optional string argument which,
|
||||
if supplied, is passed to 'parse'
|
||||
* __str__ reconstructs the string that was passed to 'parse' (or
|
||||
an equivalent string -- ie. one that will generate an equivalent
|
||||
version number instance)
|
||||
* __repr__ generates Python code to recreate the version number instance
|
||||
* _cmp compares the current instance with either another instance
|
||||
of the same class or a string (which will be parsed to an instance
|
||||
of the same class, thus must follow the same rules)
|
||||
"""
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import re
|
||||
|
||||
try:
|
||||
RE_FLAGS = re.VERBOSE | re.ASCII
|
||||
except AttributeError:
|
||||
RE_FLAGS = re.VERBOSE
|
||||
|
||||
|
||||
class Version:
|
||||
"""Abstract base class for version numbering classes. Just provides
|
||||
constructor (__init__) and reproducer (__repr__), because those
|
||||
seem to be the same for all version numbering classes; and route
|
||||
rich comparisons to _cmp.
|
||||
"""
|
||||
|
||||
def __init__(self, vstring=None):
|
||||
if vstring:
|
||||
self.parse(vstring)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s ('%s')" % (self.__class__.__name__, str(self))
|
||||
|
||||
def __eq__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c == 0
|
||||
|
||||
def __lt__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c < 0
|
||||
|
||||
def __le__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c <= 0
|
||||
|
||||
def __gt__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c > 0
|
||||
|
||||
def __ge__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c >= 0
|
||||
|
||||
|
||||
# Interface for version-number classes -- must be implemented
|
||||
# by the following classes (the concrete ones -- Version should
|
||||
# be treated as an abstract class).
|
||||
# __init__ (string) - create and take same action as 'parse'
|
||||
# (string parameter is optional)
|
||||
# parse (string) - convert a string representation to whatever
|
||||
# internal representation is appropriate for
|
||||
# this style of version numbering
|
||||
# __str__ (self) - convert back to a string; should be very similar
|
||||
# (if not identical to) the string supplied to parse
|
||||
# __repr__ (self) - generate Python code to recreate
|
||||
# the instance
|
||||
# _cmp (self, other) - compare two version numbers ('other' may
|
||||
# be an unparsed version string, or another
|
||||
# instance of your version class)
|
||||
|
||||
|
||||
class StrictVersion(Version):
|
||||
"""Version numbering for anal retentives and software idealists.
|
||||
Implements the standard interface for version number classes as
|
||||
described above. A version number consists of two or three
|
||||
dot-separated numeric components, with an optional "pre-release" tag
|
||||
on the end. The pre-release tag consists of the letter 'a' or 'b'
|
||||
followed by a number. If the numeric components of two version
|
||||
numbers are equal, then one with a pre-release tag will always
|
||||
be deemed earlier (lesser) than one without.
|
||||
|
||||
The following are valid version numbers (shown in the order that
|
||||
would be obtained by sorting according to the supplied cmp function):
|
||||
|
||||
0.4 0.4.0 (these two are equivalent)
|
||||
0.4.1
|
||||
0.5a1
|
||||
0.5b3
|
||||
0.5
|
||||
0.9.6
|
||||
1.0
|
||||
1.0.4a3
|
||||
1.0.4b1
|
||||
1.0.4
|
||||
|
||||
The following are examples of invalid version numbers:
|
||||
|
||||
1
|
||||
2.7.2.2
|
||||
1.3.a4
|
||||
1.3pl1
|
||||
1.3c4
|
||||
|
||||
The rationale for this version numbering system will be explained
|
||||
in the distutils documentation.
|
||||
"""
|
||||
|
||||
version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$',
|
||||
RE_FLAGS)
|
||||
|
||||
def parse(self, vstring):
|
||||
match = self.version_re.match(vstring)
|
||||
if not match:
|
||||
raise ValueError("invalid version number '%s'" % vstring)
|
||||
|
||||
(major, minor, patch, prerelease, prerelease_num) = \
|
||||
match.group(1, 2, 4, 5, 6)
|
||||
|
||||
if patch:
|
||||
self.version = tuple(map(int, [major, minor, patch]))
|
||||
else:
|
||||
self.version = tuple(map(int, [major, minor])) + (0,)
|
||||
|
||||
if prerelease:
|
||||
self.prerelease = (prerelease[0], int(prerelease_num))
|
||||
else:
|
||||
self.prerelease = None
|
||||
|
||||
def __str__(self):
|
||||
if self.version[2] == 0:
|
||||
vstring = '.'.join(map(str, self.version[0:2]))
|
||||
else:
|
||||
vstring = '.'.join(map(str, self.version))
|
||||
|
||||
if self.prerelease:
|
||||
vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
|
||||
|
||||
return vstring
|
||||
|
||||
def _cmp(self, other):
|
||||
if isinstance(other, str):
|
||||
other = StrictVersion(other)
|
||||
elif not isinstance(other, StrictVersion):
|
||||
return NotImplemented
|
||||
|
||||
if self.version != other.version:
|
||||
# numeric versions don't match
|
||||
# prerelease stuff doesn't matter
|
||||
if self.version < other.version:
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
|
||||
# have to compare prerelease
|
||||
# case 1: neither has prerelease; they're equal
|
||||
# case 2: self has prerelease, other doesn't; other is greater
|
||||
# case 3: self doesn't have prerelease, other does: self is greater
|
||||
# case 4: both have prerelease: must compare them!
|
||||
|
||||
if (not self.prerelease and not other.prerelease):
|
||||
return 0
|
||||
elif (self.prerelease and not other.prerelease):
|
||||
return -1
|
||||
elif (not self.prerelease and other.prerelease):
|
||||
return 1
|
||||
elif (self.prerelease and other.prerelease):
|
||||
if self.prerelease == other.prerelease:
|
||||
return 0
|
||||
elif self.prerelease < other.prerelease:
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
else:
|
||||
raise AssertionError("never get here")
|
||||
|
||||
# end class StrictVersion
|
||||
|
||||
# The rules according to Greg Stein:
|
||||
# 1) a version number has 1 or more numbers separated by a period or by
|
||||
# sequences of letters. If only periods, then these are compared
|
||||
# left-to-right to determine an ordering.
|
||||
# 2) sequences of letters are part of the tuple for comparison and are
|
||||
# compared lexicographically
|
||||
# 3) recognize the numeric components may have leading zeroes
|
||||
#
|
||||
# The LooseVersion class below implements these rules: a version number
|
||||
# string is split up into a tuple of integer and string components, and
|
||||
# comparison is a simple tuple comparison. This means that version
|
||||
# numbers behave in a predictable and obvious way, but a way that might
|
||||
# not necessarily be how people *want* version numbers to behave. There
|
||||
# wouldn't be a problem if people could stick to purely numeric version
|
||||
# numbers: just split on period and compare the numbers as tuples.
|
||||
# However, people insist on putting letters into their version numbers;
|
||||
# the most common purpose seems to be:
|
||||
# - indicating a "pre-release" version
|
||||
# ('alpha', 'beta', 'a', 'b', 'pre', 'p')
|
||||
# - indicating a post-release patch ('p', 'pl', 'patch')
|
||||
# but of course this can't cover all version number schemes, and there's
|
||||
# no way to know what a programmer means without asking him.
|
||||
#
|
||||
# The problem is what to do with letters (and other non-numeric
|
||||
# characters) in a version number. The current implementation does the
|
||||
# obvious and predictable thing: keep them as strings and compare
|
||||
# lexically within a tuple comparison. This has the desired effect if
|
||||
# an appended letter sequence implies something "post-release":
|
||||
# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
|
||||
#
|
||||
# However, if letters in a version number imply a pre-release version,
|
||||
# the "obvious" thing isn't correct. Eg. you would expect that
|
||||
# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
|
||||
# implemented here, this just isn't so.
|
||||
#
|
||||
# Two possible solutions come to mind. The first is to tie the
|
||||
# comparison algorithm to a particular set of semantic rules, as has
|
||||
# been done in the StrictVersion class above. This works great as long
|
||||
# as everyone can go along with bondage and discipline. Hopefully a
|
||||
# (large) subset of Python module programmers will agree that the
|
||||
# particular flavour of bondage and discipline provided by StrictVersion
|
||||
# provides enough benefit to be worth using, and will submit their
|
||||
# version numbering scheme to its domination. The free-thinking
|
||||
# anarchists in the lot will never give in, though, and something needs
|
||||
# to be done to accommodate them.
|
||||
#
|
||||
# Perhaps a "moderately strict" version class could be implemented that
|
||||
# lets almost anything slide (syntactically), and makes some heuristic
|
||||
# assumptions about non-digits in version number strings. This could
|
||||
# sink into special-case-hell, though; if I was as talented and
|
||||
# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
|
||||
# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
|
||||
# just as happy dealing with things like "2g6" and "1.13++". I don't
|
||||
# think I'm smart enough to do it right though.
|
||||
#
|
||||
# In any case, I've coded the test suite for this module (see
|
||||
# ../test/test_version.py) specifically to fail on things like comparing
|
||||
# "1.2a2" and "1.2". That's not because the *code* is doing anything
|
||||
# wrong, it's because the simple, obvious design doesn't match my
|
||||
# complicated, hairy expectations for real-world version numbers. It
|
||||
# would be a snap to fix the test suite to say, "Yep, LooseVersion does
|
||||
# the Right Thing" (ie. the code matches the conception). But I'd rather
|
||||
# have a conception that matches common notions about version numbers.
|
||||
|
||||
|
||||
class LooseVersion(Version):
|
||||
"""Version numbering for anarchists and software realists.
|
||||
Implements the standard interface for version number classes as
|
||||
described above. A version number consists of a series of numbers,
|
||||
separated by either periods or strings of letters. When comparing
|
||||
version numbers, the numeric components will be compared
|
||||
numerically, and the alphabetic components lexically. The following
|
||||
are all valid version numbers, in no particular order:
|
||||
|
||||
1.5.1
|
||||
1.5.2b2
|
||||
161
|
||||
3.10a
|
||||
8.02
|
||||
3.4j
|
||||
1996.07.12
|
||||
3.2.pl0
|
||||
3.1.1.6
|
||||
2g6
|
||||
11g
|
||||
0.960923
|
||||
2.2beta29
|
||||
1.13++
|
||||
5.5.kw
|
||||
2.0b1pl0
|
||||
|
||||
In fact, there is no such thing as an invalid version number under
|
||||
this scheme; the rules for comparison are simple and predictable,
|
||||
but may not always give the results you want (for some definition
|
||||
of "want").
|
||||
"""
|
||||
|
||||
component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
|
||||
|
||||
def __init__(self, vstring=None):
|
||||
if vstring:
|
||||
self.parse(vstring)
|
||||
|
||||
def parse(self, vstring):
|
||||
# I've given up on thinking I can reconstruct the version string
|
||||
# from the parsed tuple -- so I just store the string here for
|
||||
# use by __str__
|
||||
self.vstring = vstring
|
||||
components = [x for x in self.component_re.split(vstring) if x and x != '.']
|
||||
for i, obj in enumerate(components):
|
||||
try:
|
||||
components[i] = int(obj)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.version = components
|
||||
|
||||
def __str__(self):
|
||||
return self.vstring
|
||||
|
||||
def __repr__(self):
|
||||
return "LooseVersion ('%s')" % str(self)
|
||||
|
||||
def _cmp(self, other):
|
||||
if isinstance(other, str):
|
||||
other = LooseVersion(other)
|
||||
elif not isinstance(other, LooseVersion):
|
||||
return NotImplemented
|
||||
|
||||
if self.version == other.version:
|
||||
return 0
|
||||
if self.version < other.version:
|
||||
return -1
|
||||
if self.version > other.version:
|
||||
return 1
|
||||
|
||||
# end class LooseVersion
|
||||
@@ -14,8 +14,9 @@ import json
|
||||
import locale
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.six import PY3
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
|
||||
OpenSSLCLIBackend,
|
||||
@@ -228,9 +229,14 @@ class ACMEClient(object):
|
||||
resp, info = fetch_url(self.module, url, data=data, headers=headers, method='POST')
|
||||
_assert_fetch_url_success(self.module, resp, info)
|
||||
result = {}
|
||||
|
||||
try:
|
||||
# In Python 2, reading from a closed response yields a TypeError.
|
||||
# In Python 3, read() simply returns ''
|
||||
if PY3 and resp.closed:
|
||||
raise TypeError
|
||||
content = resp.read()
|
||||
except AttributeError:
|
||||
except (AttributeError, TypeError):
|
||||
content = info.pop('body', None)
|
||||
|
||||
if content or not parse_json_result:
|
||||
@@ -284,8 +290,12 @@ class ACMEClient(object):
|
||||
_assert_fetch_url_success(self.module, resp, info)
|
||||
|
||||
try:
|
||||
# In Python 2, reading from a closed response yields a TypeError.
|
||||
# In Python 3, read() simply returns ''
|
||||
if PY3 and resp.closed:
|
||||
raise TypeError
|
||||
content = resp.read()
|
||||
except AttributeError:
|
||||
except (AttributeError, TypeError):
|
||||
content = info.pop('body', None)
|
||||
|
||||
# Process result
|
||||
|
||||
@@ -14,7 +14,9 @@ import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
|
||||
CryptoBackend,
|
||||
@@ -41,6 +43,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 (
|
||||
extract_first_pem,
|
||||
)
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
import cryptography.hazmat.backends
|
||||
@@ -53,7 +59,6 @@ try:
|
||||
import cryptography.hazmat.primitives.serialization
|
||||
import cryptography.x509
|
||||
import cryptography.x509.oid
|
||||
from distutils.version import LooseVersion
|
||||
CRYPTOGRAPHY_VERSION = cryptography.__version__
|
||||
HAS_CURRENT_CRYPTOGRAPHY = (LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion('1.5'))
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
@@ -357,6 +362,9 @@ class CryptographyBackend(CryptoBackend):
|
||||
if cert_content is None:
|
||||
return -1
|
||||
|
||||
# Make sure we have at most one PEM. Otherwise cryptography 36.0.0 will barf.
|
||||
cert_content = to_bytes(extract_first_pem(to_text(cert_content)) or '')
|
||||
|
||||
try:
|
||||
cert = cryptography.x509.load_pem_x509_certificate(cert_content, _cryptography_backend)
|
||||
except Exception as e:
|
||||
|
||||
@@ -230,7 +230,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
filename = csr_filename
|
||||
data = None
|
||||
if csr_content is not None:
|
||||
filename = '-'
|
||||
filename = '/dev/stdin'
|
||||
data = csr_content.encode('utf-8')
|
||||
|
||||
openssl_csr_cmd = [self.openssl_binary, "req", "-in", filename, "-noout", "-text"]
|
||||
@@ -267,7 +267,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
filename = cert_filename
|
||||
data = None
|
||||
if cert_content is not None:
|
||||
filename = '-'
|
||||
filename = '/dev/stdin'
|
||||
data = cert_content.encode('utf-8')
|
||||
cert_filename_suffix = ''
|
||||
elif cert_filename is not None:
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.module_utils.six import binary_type
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
from ansible.module_utils.six import binary_type, PY3
|
||||
|
||||
|
||||
def format_error_problem(problem, subproblem_prefix=''):
|
||||
@@ -52,8 +52,12 @@ class ACMEProtocolException(ModuleFailException):
|
||||
# Try to get hold of content, if response is given and content is not provided
|
||||
if content is None and content_json is None and response is not None:
|
||||
try:
|
||||
# In Python 2, reading from a closed response yields a TypeError.
|
||||
# In Python 3, read() simply returns ''
|
||||
if PY3 and response.closed:
|
||||
raise TypeError
|
||||
content = response.read()
|
||||
except AttributeError:
|
||||
except (AttributeError, TypeError):
|
||||
content = info.pop('body', None)
|
||||
|
||||
# Make sure that content_json is None or a dictionary
|
||||
|
||||
@@ -20,6 +20,10 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
# WARNING: this function no longer works with cryptography 35.0.0 and newer!
|
||||
# It must **ONLY** be used in compatibility code for older
|
||||
# cryptography versions!
|
||||
|
||||
def obj2txt(openssl_lib, openssl_ffi, obj):
|
||||
# Set to 80 on the recommendation of
|
||||
# https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values
|
||||
|
||||
@@ -20,7 +20,7 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
try:
|
||||
import OpenSSL # noqa
|
||||
|
||||
@@ -26,9 +26,12 @@ import re
|
||||
from ansible.module_utils.common.text.converters import to_text, to_bytes
|
||||
from ._asn1 import serialize_asn1_string_as_der
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
@@ -44,6 +47,15 @@ except ImportError:
|
||||
# Error handled in the calling module.
|
||||
_load_key_and_certificates = None
|
||||
|
||||
try:
|
||||
# This is a separate try/except since this is only present in cryptography 36.0.0 or newer
|
||||
from cryptography.hazmat.primitives.serialization.pkcs12 import (
|
||||
load_pkcs12 as _load_pkcs12,
|
||||
)
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
_load_pkcs12 = None
|
||||
|
||||
from .basic import (
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
@@ -64,60 +76,114 @@ DOTTED_OID = re.compile(r'^\d+(?:\.\d+)+$')
|
||||
|
||||
|
||||
def cryptography_get_extensions_from_cert(cert):
|
||||
# Since cryptography won't give us the DER value for an extension
|
||||
# (that is only stored for unrecognized extensions), we have to re-do
|
||||
# the extension parsing outselves.
|
||||
result = dict()
|
||||
backend = cert._backend
|
||||
x509_obj = cert._x509
|
||||
try:
|
||||
# Since cryptography won't give us the DER value for an extension
|
||||
# (that is only stored for unrecognized extensions), we have to re-do
|
||||
# the extension parsing outselves.
|
||||
backend = default_backend()
|
||||
try:
|
||||
# For certain old versions of cryptography, backend is a MultiBackend object,
|
||||
# which has no _lib attribute. In that case, revert to the old approach.
|
||||
backend._lib
|
||||
except AttributeError:
|
||||
backend = cert._backend
|
||||
|
||||
x509_obj = cert._x509
|
||||
# With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does
|
||||
# not allow to get the raw value of an extension, so we have to use this ugly hack:
|
||||
exts = list(cert.extensions)
|
||||
|
||||
for i in range(backend._lib.X509_get_ext_count(x509_obj)):
|
||||
ext = backend._lib.X509_get_ext(x509_obj, i)
|
||||
if ext == backend._ffi.NULL:
|
||||
continue
|
||||
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
||||
data = backend._lib.X509_EXTENSION_get_data(ext)
|
||||
backend.openssl_assert(data != backend._ffi.NULL)
|
||||
der = backend._ffi.buffer(data.data, data.length)[:]
|
||||
entry = dict(
|
||||
critical=(crit == 1),
|
||||
value=base64.b64encode(der),
|
||||
)
|
||||
try:
|
||||
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
|
||||
except AttributeError:
|
||||
oid = exts[i].oid.dotted_string
|
||||
result[oid] = entry
|
||||
|
||||
except Exception:
|
||||
# In case the above method breaks, we likely have cryptography 36.0.0 or newer.
|
||||
# Use it's public_bytes() feature in that case. We will later switch this around
|
||||
# so that this code will be the default, but for now this will act as a fallback
|
||||
# since it will re-serialize de-serialized data, which can be different (if the
|
||||
# original data was not canonicalized) from what was contained in the certificate.
|
||||
for ext in cert.extensions:
|
||||
result[ext.oid.dotted_string] = dict(
|
||||
critical=ext.critical,
|
||||
value=base64.b64encode(ext.value.public_bytes()),
|
||||
)
|
||||
|
||||
for i in range(backend._lib.X509_get_ext_count(x509_obj)):
|
||||
ext = backend._lib.X509_get_ext(x509_obj, i)
|
||||
if ext == backend._ffi.NULL:
|
||||
continue
|
||||
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
||||
data = backend._lib.X509_EXTENSION_get_data(ext)
|
||||
backend.openssl_assert(data != backend._ffi.NULL)
|
||||
der = backend._ffi.buffer(data.data, data.length)[:]
|
||||
entry = dict(
|
||||
critical=(crit == 1),
|
||||
value=base64.b64encode(der),
|
||||
)
|
||||
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
|
||||
result[oid] = entry
|
||||
return result
|
||||
|
||||
|
||||
def cryptography_get_extensions_from_csr(csr):
|
||||
# Since cryptography won't give us the DER value for an extension
|
||||
# (that is only stored for unrecognized extensions), we have to re-do
|
||||
# the extension parsing outselves.
|
||||
result = dict()
|
||||
backend = csr._backend
|
||||
try:
|
||||
# Since cryptography won't give us the DER value for an extension
|
||||
# (that is only stored for unrecognized extensions), we have to re-do
|
||||
# the extension parsing outselves.
|
||||
backend = default_backend()
|
||||
try:
|
||||
# For certain old versions of cryptography, backend is a MultiBackend object,
|
||||
# which has no _lib attribute. In that case, revert to the old approach.
|
||||
backend._lib
|
||||
except AttributeError:
|
||||
backend = csr._backend
|
||||
|
||||
extensions = backend._lib.X509_REQ_get_extensions(csr._x509_req)
|
||||
extensions = backend._ffi.gc(
|
||||
extensions,
|
||||
lambda ext: backend._lib.sk_X509_EXTENSION_pop_free(
|
||||
ext,
|
||||
backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free")
|
||||
extensions = backend._lib.X509_REQ_get_extensions(csr._x509_req)
|
||||
extensions = backend._ffi.gc(
|
||||
extensions,
|
||||
lambda ext: backend._lib.sk_X509_EXTENSION_pop_free(
|
||||
ext,
|
||||
backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)):
|
||||
ext = backend._lib.sk_X509_EXTENSION_value(extensions, i)
|
||||
if ext == backend._ffi.NULL:
|
||||
continue
|
||||
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
||||
data = backend._lib.X509_EXTENSION_get_data(ext)
|
||||
backend.openssl_assert(data != backend._ffi.NULL)
|
||||
der = backend._ffi.buffer(data.data, data.length)[:]
|
||||
entry = dict(
|
||||
critical=(crit == 1),
|
||||
value=base64.b64encode(der),
|
||||
)
|
||||
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
|
||||
result[oid] = entry
|
||||
# With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does
|
||||
# not allow to get the raw value of an extension, so we have to use this ugly hack:
|
||||
exts = list(csr.extensions)
|
||||
|
||||
for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)):
|
||||
ext = backend._lib.sk_X509_EXTENSION_value(extensions, i)
|
||||
if ext == backend._ffi.NULL:
|
||||
continue
|
||||
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
||||
data = backend._lib.X509_EXTENSION_get_data(ext)
|
||||
backend.openssl_assert(data != backend._ffi.NULL)
|
||||
der = backend._ffi.buffer(data.data, data.length)[:]
|
||||
entry = dict(
|
||||
critical=(crit == 1),
|
||||
value=base64.b64encode(der),
|
||||
)
|
||||
try:
|
||||
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
|
||||
except AttributeError:
|
||||
oid = exts[i].oid.dotted_string
|
||||
result[oid] = entry
|
||||
|
||||
except Exception:
|
||||
# In case the above method breaks, we likely have cryptography 36.0.0 or newer.
|
||||
# Use it's public_bytes() feature in that case. We will later switch this around
|
||||
# so that this code will be the default, but for now this will act as a fallback
|
||||
# since it will re-serialize de-serialized data, which can be different (if the
|
||||
# original data was not canonicalized) from what was contained in the CSR.
|
||||
for ext in csr.extensions:
|
||||
result[ext.oid.dotted_string] = dict(
|
||||
critical=ext.critical,
|
||||
value=base64.b64encode(ext.value.public_bytes()),
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -280,11 +346,11 @@ def _dn_escape_value(value):
|
||||
'''
|
||||
Escape Distinguished Name's attribute value.
|
||||
'''
|
||||
value = value.replace('\\', '\\\\')
|
||||
for ch in [',', '#', '+', '<', '>', ';', '"', '=', '/']:
|
||||
value = value.replace(ch, '\\%s' % ch)
|
||||
if value.startswith(' '):
|
||||
value = r'\ ' + value[1:]
|
||||
value = value.replace(u'\\', u'\\\\')
|
||||
for ch in [u',', u'#', u'+', u'<', u'>', u';', u'"', u'=', u'/']:
|
||||
value = value.replace(ch, u'\\%s' % ch)
|
||||
if value.startswith(u' '):
|
||||
value = u'\\ ' + value[1:]
|
||||
return value
|
||||
|
||||
|
||||
@@ -294,24 +360,24 @@ def cryptography_decode_name(name):
|
||||
Raises an OpenSSLObjectError if the name is not supported.
|
||||
'''
|
||||
if isinstance(name, x509.DNSName):
|
||||
return 'DNS:{0}'.format(name.value)
|
||||
return u'DNS:{0}'.format(name.value)
|
||||
if isinstance(name, x509.IPAddress):
|
||||
if isinstance(name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network)):
|
||||
return 'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen)
|
||||
return 'IP:{0}'.format(name.value.compressed)
|
||||
return u'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen)
|
||||
return u'IP:{0}'.format(name.value.compressed)
|
||||
if isinstance(name, x509.RFC822Name):
|
||||
return 'email:{0}'.format(name.value)
|
||||
return u'email:{0}'.format(name.value)
|
||||
if isinstance(name, x509.UniformResourceIdentifier):
|
||||
return 'URI:{0}'.format(name.value)
|
||||
return u'URI:{0}'.format(name.value)
|
||||
if isinstance(name, x509.DirectoryName):
|
||||
return 'dirName:' + ''.join([
|
||||
'/{0}={1}'.format(cryptography_oid_to_name(attribute.oid, short=True), _dn_escape_value(attribute.value))
|
||||
return u'dirName:' + u''.join([
|
||||
u'/{0}={1}'.format(to_text(cryptography_oid_to_name(attribute.oid, short=True)), _dn_escape_value(attribute.value))
|
||||
for attribute in name.value
|
||||
])
|
||||
if isinstance(name, x509.RegisteredID):
|
||||
return 'RID:{0}'.format(name.value.dotted_string)
|
||||
return u'RID:{0}'.format(name.value.dotted_string)
|
||||
if isinstance(name, x509.OtherName):
|
||||
return 'otherName:{0};{1}'.format(name.type_id.dotted_string, _get_hex(name.value))
|
||||
return u'otherName:{0};{1}'.format(name.type_id.dotted_string, _get_hex(name.value))
|
||||
raise OpenSSLObjectError('Cannot decode name "{0}"'.format(name))
|
||||
|
||||
|
||||
@@ -442,17 +508,74 @@ def cryptography_serial_number_of_cert(cert):
|
||||
def parse_pkcs12(pkcs12_bytes, passphrase=None):
|
||||
'''Returns a tuple (private_key, certificate, additional_certificates, friendly_name).
|
||||
'''
|
||||
if _load_key_and_certificates is None:
|
||||
raise ValueError('load_key_and_certificates() not present in the current cryptography version')
|
||||
private_key, certificate, additional_certificates = _load_key_and_certificates(
|
||||
pkcs12_bytes, to_bytes(passphrase) if passphrase is not None else None)
|
||||
if _load_pkcs12 is None and _load_key_and_certificates is None:
|
||||
raise ValueError('neither load_pkcs12() nor load_key_and_certificates() present in the current cryptography version')
|
||||
|
||||
if passphrase is not None:
|
||||
passphrase = to_bytes(passphrase)
|
||||
|
||||
# Main code for cryptography 36.0.0 and forward
|
||||
if _load_pkcs12 is not None:
|
||||
return _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase)
|
||||
|
||||
if LooseVersion(cryptography.__version__) >= LooseVersion('35.0'):
|
||||
return _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase)
|
||||
|
||||
return _parse_pkcs12_legacy(pkcs12_bytes, passphrase)
|
||||
|
||||
|
||||
def _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase=None):
|
||||
# Requires cryptography 36.0.0 or newer
|
||||
pkcs12 = _load_pkcs12(pkcs12_bytes, passphrase)
|
||||
additional_certificates = [cert.certificate for cert in pkcs12.additional_certs]
|
||||
private_key = pkcs12.key
|
||||
certificate = None
|
||||
friendly_name = None
|
||||
if pkcs12.cert:
|
||||
certificate = pkcs12.cert.certificate
|
||||
friendly_name = pkcs12.cert.friendly_name
|
||||
return private_key, certificate, additional_certificates, friendly_name
|
||||
|
||||
|
||||
def _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase=None):
|
||||
# Backwards compatibility code for cryptography 35.x
|
||||
private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase)
|
||||
|
||||
friendly_name = None
|
||||
if certificate:
|
||||
# See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238
|
||||
maybe_name = certificate._backend._lib.X509_alias_get0(
|
||||
certificate._x509, certificate._backend._ffi.NULL)
|
||||
if maybe_name != certificate._backend._ffi.NULL:
|
||||
friendly_name = certificate._backend._ffi.string(maybe_name)
|
||||
backend = default_backend()
|
||||
|
||||
# This code basically does what load_key_and_certificates() does, but without error-checking.
|
||||
# Since load_key_and_certificates succeeded, it should not fail.
|
||||
pkcs12 = backend._ffi.gc(
|
||||
backend._lib.d2i_PKCS12_bio(backend._bytes_to_bio(pkcs12_bytes).bio, backend._ffi.NULL),
|
||||
backend._lib.PKCS12_free)
|
||||
certificate_x509_ptr = backend._ffi.new("X509 **")
|
||||
with backend._zeroed_null_terminated_buf(to_bytes(passphrase) if passphrase is not None else None) as passphrase_buffer:
|
||||
backend._lib.PKCS12_parse(
|
||||
pkcs12,
|
||||
passphrase_buffer,
|
||||
backend._ffi.new("EVP_PKEY **"),
|
||||
certificate_x509_ptr,
|
||||
backend._ffi.new("Cryptography_STACK_OF_X509 **"))
|
||||
if certificate_x509_ptr[0] != backend._ffi.NULL:
|
||||
maybe_name = backend._lib.X509_alias_get0(certificate_x509_ptr[0], backend._ffi.NULL)
|
||||
if maybe_name != backend._ffi.NULL:
|
||||
friendly_name = backend._ffi.string(maybe_name)
|
||||
|
||||
return private_key, certificate, additional_certificates, friendly_name
|
||||
|
||||
|
||||
def _parse_pkcs12_legacy(pkcs12_bytes, passphrase=None):
|
||||
# Backwards compatibility code for cryptography < 35.0.0
|
||||
private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase)
|
||||
|
||||
friendly_name = None
|
||||
if certificate:
|
||||
# See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238
|
||||
backend = certificate._backend
|
||||
maybe_name = backend._lib.X509_alias_get0(certificate._x509, backend._ffi.NULL)
|
||||
if maybe_name != backend._ffi.NULL:
|
||||
friendly_name = backend._ffi.string(maybe_name)
|
||||
return private_key, certificate, additional_certificates, friendly_name
|
||||
|
||||
@@ -11,11 +11,11 @@ __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.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
|
||||
@@ -15,12 +15,12 @@ import datetime
|
||||
import re
|
||||
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.common.text.converters import to_native, to_text, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
load_certificate,
|
||||
get_fingerprint_of_bytes,
|
||||
|
||||
@@ -10,11 +10,12 @@ __metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from random import randrange
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLBadPassphraseError,
|
||||
)
|
||||
|
||||
@@ -9,10 +9,10 @@ __metaclass__ = type
|
||||
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_oid_to_name,
|
||||
)
|
||||
|
||||
@@ -12,12 +12,12 @@ 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.common.text.converters import to_bytes, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
OpenSSLBadPassphraseError,
|
||||
@@ -592,7 +592,7 @@ class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBack
|
||||
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]
|
||||
subject = [(cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.subject]
|
||||
current_subject = [(sub.oid, sub.value) for sub in csr.subject]
|
||||
return set(subject) == set(current_subject)
|
||||
|
||||
@@ -604,8 +604,8 @@ class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBack
|
||||
|
||||
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 []
|
||||
current_altnames = [to_text(altname) for altname in current_altnames_ext.value] if current_altnames_ext else []
|
||||
altnames = [to_text(cryptography_get_name(altname)) for altname in self.subjectAltName] if self.subjectAltName else []
|
||||
if set(altnames) != set(current_altnames):
|
||||
return False
|
||||
if altnames:
|
||||
@@ -678,10 +678,10 @@ class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBack
|
||||
|
||||
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]
|
||||
current_nc_perm = [to_text(altname) for altname in current_nc_ext.value.permitted_subtrees] if current_nc_ext else []
|
||||
current_nc_excl = [to_text(altname) for altname in current_nc_ext.value.excluded_subtrees] if current_nc_ext else []
|
||||
nc_perm = [to_text(cryptography_get_name(altname, 'name constraints permitted')) for altname in self.name_constraints_permitted]
|
||||
nc_excl = [to_text(cryptography_get_name(altname, 'name constraints excluded')) for altname in self.name_constraints_excluded]
|
||||
if set(nc_perm) != set(current_nc_perm) or set(nc_excl) != set(current_nc_excl):
|
||||
return False
|
||||
if nc_perm or nc_excl:
|
||||
@@ -710,9 +710,9 @@ class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBack
|
||||
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]
|
||||
aci = [to_text(cryptography_get_name(n, 'authority cert issuer')) for n in self.authority_cert_issuer]
|
||||
if ext.value.authority_cert_issuer is not None:
|
||||
csr_aci = [str(n) for n in ext.value.authority_cert_issuer]
|
||||
csr_aci = [to_text(n) for n in ext.value.authority_cert_issuer]
|
||||
return (ext.value.key_identifier == self.authority_key_identifier
|
||||
and csr_aci == aci
|
||||
and ext.value.authority_cert_serial_number == self.authority_cert_serial_number)
|
||||
|
||||
@@ -13,12 +13,12 @@ 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.common.text.converters import to_native, to_text, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
load_certificate_request,
|
||||
get_fingerprint_of_bytes,
|
||||
|
||||
@@ -12,12 +12,12 @@ 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.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_X25519,
|
||||
CRYPTOGRAPHY_HAS_X25519_FULL,
|
||||
|
||||
@@ -12,12 +12,12 @@ __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.module_utils.common.text.converters import to_native, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
|
||||
@@ -10,12 +10,12 @@ __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.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_X25519,
|
||||
CRYPTOGRAPHY_HAS_X448,
|
||||
|
||||
@@ -72,3 +72,13 @@ def split_pem_list(text, keep_inbetween=False):
|
||||
result.append(''.join(current))
|
||||
current = [] if keep_inbetween else None
|
||||
return result
|
||||
|
||||
|
||||
def extract_first_pem(text):
|
||||
'''
|
||||
Given one PEM or multiple concatenated PEM objects, return only the first one, or None if there is none.
|
||||
'''
|
||||
all_pems = split_pem_list(text)
|
||||
if not all_pems:
|
||||
return None
|
||||
return all_pems[0]
|
||||
|
||||
@@ -21,10 +21,12 @@ __metaclass__ = type
|
||||
|
||||
import base64
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
||||
|
||||
from ._objects import OID_LOOKUP
|
||||
|
||||
try:
|
||||
import OpenSSL
|
||||
except ImportError:
|
||||
@@ -87,18 +89,25 @@ def pyopenssl_get_extensions_from_cert(cert):
|
||||
critical=bool(ext.get_critical()),
|
||||
value=base64.b64encode(ext.get_data()),
|
||||
)
|
||||
oid = obj2txt(
|
||||
OpenSSL._util.lib,
|
||||
OpenSSL._util.ffi,
|
||||
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
||||
)
|
||||
# This could also be done a bit simpler:
|
||||
#
|
||||
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
|
||||
#
|
||||
# Unfortunately this gives the wrong result in case the linked OpenSSL
|
||||
# doesn't know the OID. That's why we have to get the OID dotted string
|
||||
# similarly to how cryptography does it.
|
||||
try:
|
||||
oid = obj2txt(
|
||||
OpenSSL._util.lib,
|
||||
OpenSSL._util.ffi,
|
||||
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
||||
)
|
||||
# This could also be done a bit simpler:
|
||||
#
|
||||
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
|
||||
#
|
||||
# Unfortunately this gives the wrong result in case the linked OpenSSL
|
||||
# doesn't know the OID. That's why we have to get the OID dotted string
|
||||
# similarly to how cryptography does it.
|
||||
except AttributeError:
|
||||
# When PyOpenSSL is used with cryptography >= 35.0.0, obj2txt cannot be used.
|
||||
# We try to figure out the OID with our internal lookup table, and if we fail,
|
||||
# we use the short name OpenSSL returns.
|
||||
oid = to_native(ext.get_short_name())
|
||||
oid = OID_LOOKUP.get(oid, oid)
|
||||
result[oid] = entry
|
||||
return result
|
||||
|
||||
@@ -113,18 +122,25 @@ def pyopenssl_get_extensions_from_csr(csr):
|
||||
critical=bool(ext.get_critical()),
|
||||
value=base64.b64encode(ext.get_data()),
|
||||
)
|
||||
oid = obj2txt(
|
||||
OpenSSL._util.lib,
|
||||
OpenSSL._util.ffi,
|
||||
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
||||
)
|
||||
# This could also be done a bit simpler:
|
||||
#
|
||||
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
|
||||
#
|
||||
# Unfortunately this gives the wrong result in case the linked OpenSSL
|
||||
# doesn't know the OID. That's why we have to get the OID dotted string
|
||||
# similarly to how cryptography does it.
|
||||
try:
|
||||
oid = obj2txt(
|
||||
OpenSSL._util.lib,
|
||||
OpenSSL._util.ffi,
|
||||
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
||||
)
|
||||
# This could also be done a bit simpler:
|
||||
#
|
||||
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
|
||||
#
|
||||
# Unfortunately this gives the wrong result in case the linked OpenSSL
|
||||
# doesn't know the OID. That's why we have to get the OID dotted string
|
||||
# similarly to how cryptography does it.
|
||||
except AttributeError:
|
||||
# When PyOpenSSL is used with cryptography >= 35.0.0, obj2txt cannot be used.
|
||||
# We try to figure out the OID with our internal lookup table, and if we fail,
|
||||
# we use the short name OpenSSL returns.
|
||||
oid = to_native(ext.get_short_name())
|
||||
oid = OID_LOOKUP.get(oid, oid)
|
||||
result[oid] = entry
|
||||
return result
|
||||
|
||||
|
||||
@@ -18,7 +18,15 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import abc
|
||||
import os
|
||||
import stat
|
||||
|
||||
from ansible.module_utils import six
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
|
||||
parse_openssh_version,
|
||||
)
|
||||
|
||||
|
||||
def restore_on_failure(f):
|
||||
@@ -40,3 +48,281 @@ def restore_on_failure(f):
|
||||
@restore_on_failure
|
||||
def safe_atomic_move(module, path, destination):
|
||||
module.atomic_move(path, destination)
|
||||
|
||||
|
||||
def _restore_all_on_failure(f):
|
||||
def backup_and_restore(self, sources_and_destinations, *args, **kwargs):
|
||||
backups = [(d, self.module.backup_local(d)) for s, d in sources_and_destinations if os.path.exists(d)]
|
||||
|
||||
try:
|
||||
f(self, sources_and_destinations, *args, **kwargs)
|
||||
except Exception:
|
||||
for destination, backup in backups:
|
||||
self.module.atomic_move(backup, destination)
|
||||
raise
|
||||
else:
|
||||
for destination, backup in backups:
|
||||
self.module.add_cleanup_file(backup)
|
||||
return backup_and_restore
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class OpensshModule(object):
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
self.changed = False
|
||||
self.check_mode = self.module.check_mode
|
||||
|
||||
def execute(self):
|
||||
self._execute()
|
||||
self.module.exit_json(**self.result)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _execute(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
result = self._result
|
||||
|
||||
result['changed'] = self.changed
|
||||
|
||||
if self.module._diff:
|
||||
result['diff'] = self.diff
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def _result(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def diff(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def skip_if_check_mode(f):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if not self.check_mode:
|
||||
f(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
@staticmethod
|
||||
def trigger_change(f):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
f(self, *args, **kwargs)
|
||||
self.changed = True
|
||||
return wrapper
|
||||
|
||||
def _check_if_base_dir(self, path):
|
||||
base_dir = os.path.dirname(path) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
self.module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
|
||||
def _get_ssh_version(self):
|
||||
ssh_bin = self.module.get_bin_path('ssh')
|
||||
if not ssh_bin:
|
||||
return ""
|
||||
return parse_openssh_version(self.module.run_command([ssh_bin, '-V', '-q'])[2].strip())
|
||||
|
||||
@_restore_all_on_failure
|
||||
def _safe_secure_move(self, sources_and_destinations):
|
||||
"""Moves a list of files from 'source' to 'destination' and restores 'destination' from backup upon failure.
|
||||
If 'destination' does not already exist, then 'source' permissions are preserved to prevent
|
||||
exposing protected data ('atomic_move' uses the 'destination' base directory mask for
|
||||
permissions if 'destination' does not already exists).
|
||||
"""
|
||||
for source, destination in sources_and_destinations:
|
||||
if os.path.exists(destination):
|
||||
self.module.atomic_move(source, destination)
|
||||
else:
|
||||
self.module.preserved_copy(source, destination)
|
||||
|
||||
def _update_permissions(self, path):
|
||||
file_args = self.module.load_file_common_arguments(self.module.params)
|
||||
file_args['path'] = path
|
||||
|
||||
if not self.module.check_file_absent_if_check_mode(path):
|
||||
self.changed = self.module.set_fs_attributes_if_different(file_args, self.changed)
|
||||
else:
|
||||
self.changed = True
|
||||
|
||||
|
||||
class KeygenCommand(object):
|
||||
def __init__(self, module):
|
||||
self._bin_path = module.get_bin_path('ssh-keygen', True)
|
||||
self._run_command = module.run_command
|
||||
|
||||
def generate_certificate(self, certificate_path, identifier, options, pkcs11_provider, principals,
|
||||
serial_number, signature_algorithm, signing_key_path, type,
|
||||
time_parameters, use_agent, **kwargs):
|
||||
args = [self._bin_path, '-s', signing_key_path, '-P', '', '-I', identifier]
|
||||
|
||||
if options:
|
||||
for option in options:
|
||||
args.extend(['-O', option])
|
||||
if pkcs11_provider:
|
||||
args.extend(['-D', pkcs11_provider])
|
||||
if principals:
|
||||
args.extend(['-n', ','.join(principals)])
|
||||
if serial_number is not None:
|
||||
args.extend(['-z', str(serial_number)])
|
||||
if type == 'host':
|
||||
args.extend(['-h'])
|
||||
if use_agent:
|
||||
args.extend(['-U'])
|
||||
if time_parameters.validity_string:
|
||||
args.extend(['-V', time_parameters.validity_string])
|
||||
if signature_algorithm:
|
||||
args.extend(['-t', signature_algorithm])
|
||||
args.append(certificate_path)
|
||||
|
||||
return self._run_command(args, **kwargs)
|
||||
|
||||
def generate_keypair(self, private_key_path, size, type, comment, **kwargs):
|
||||
args = [
|
||||
self._bin_path,
|
||||
'-q',
|
||||
'-N', '',
|
||||
'-b', str(size),
|
||||
'-t', type,
|
||||
'-f', private_key_path,
|
||||
'-C', comment or ''
|
||||
]
|
||||
|
||||
# "y" must be entered in response to the "overwrite" prompt
|
||||
data = 'y' if os.path.exists(private_key_path) else None
|
||||
|
||||
return self._run_command(args, data=data, **kwargs)
|
||||
|
||||
def get_certificate_info(self, certificate_path, **kwargs):
|
||||
return self._run_command([self._bin_path, '-L', '-f', certificate_path], **kwargs)
|
||||
|
||||
def get_matching_public_key(self, private_key_path, **kwargs):
|
||||
return self._run_command([self._bin_path, '-P', '', '-y', '-f', private_key_path], **kwargs)
|
||||
|
||||
def get_private_key(self, private_key_path, **kwargs):
|
||||
return self._run_command([self._bin_path, '-l', '-f', private_key_path], **kwargs)
|
||||
|
||||
def update_comment(self, private_key_path, comment, **kwargs):
|
||||
if os.path.exists(private_key_path) and not os.access(private_key_path, os.W_OK):
|
||||
try:
|
||||
os.chmod(private_key_path, stat.S_IWUSR + stat.S_IRUSR)
|
||||
except (IOError, OSError) as e:
|
||||
raise e("The private key at %s is not writeable preventing a comment update" % private_key_path)
|
||||
|
||||
return self._run_command([self._bin_path, '-q', '-o', '-c', '-C', comment, '-f', private_key_path], **kwargs)
|
||||
|
||||
|
||||
class PrivateKey(object):
|
||||
def __init__(self, size, key_type, fingerprint):
|
||||
self._size = size
|
||||
self._type = key_type
|
||||
self._fingerprint = fingerprint
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def fingerprint(self):
|
||||
return self._fingerprint
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, string):
|
||||
properties = string.split()
|
||||
|
||||
return cls(
|
||||
size=int(properties[0]),
|
||||
key_type=properties[-1][1:-1].lower(),
|
||||
fingerprint=properties[1],
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'size': self._size,
|
||||
'type': self._type,
|
||||
'fingerprint': self._fingerprint,
|
||||
}
|
||||
|
||||
|
||||
class PublicKey(object):
|
||||
def __init__(self, type_string, data, comment):
|
||||
self._type_string = type_string
|
||||
self._data = data
|
||||
self._comment = comment
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, type(self)):
|
||||
return NotImplemented
|
||||
|
||||
return all([
|
||||
self._type_string == other._type_string,
|
||||
self._data == other._data,
|
||||
(self._comment == other._comment) if self._comment is not None and other._comment is not None else True
|
||||
])
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s" % (self._type_string, self._data)
|
||||
|
||||
@property
|
||||
def comment(self):
|
||||
return self._comment
|
||||
|
||||
@comment.setter
|
||||
def comment(self, value):
|
||||
self._comment = value
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def type_string(self):
|
||||
return self._type_string
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, string):
|
||||
properties = string.strip('\n').split(' ', 2)
|
||||
|
||||
return cls(
|
||||
type_string=properties[0],
|
||||
data=properties[1],
|
||||
comment=properties[2] if len(properties) > 2 else ""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load(cls, path):
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
properties = f.read().strip(' \n').split(' ', 2)
|
||||
except (IOError, OSError):
|
||||
raise
|
||||
|
||||
if len(properties) < 2:
|
||||
return None
|
||||
|
||||
return cls(
|
||||
type_string=properties[0],
|
||||
data=properties[1],
|
||||
comment='' if len(properties) <= 2 else properties[2],
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'comment': self._comment,
|
||||
'public_key': self._data,
|
||||
}
|
||||
|
||||
@@ -20,16 +20,14 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import abc
|
||||
import errno
|
||||
import os
|
||||
import stat
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import parse_openssh_version
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.cryptography import (
|
||||
HAS_OPENSSH_SUPPORT,
|
||||
HAS_OPENSSH_PRIVATE_FORMAT,
|
||||
@@ -39,322 +37,334 @@ from ansible_collections.community.crypto.plugins.module_utils.openssh.cryptogra
|
||||
OpenSSHError,
|
||||
OpensshKeypair,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import (
|
||||
KeygenCommand,
|
||||
OpensshModule,
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
|
||||
any_in,
|
||||
file_mode,
|
||||
secure_write,
|
||||
)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class KeypairBackend(object):
|
||||
class KeypairBackend(OpensshModule):
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
super(KeypairBackend, self).__init__(module)
|
||||
|
||||
self.path = module.params['path']
|
||||
self.force = module.params['force']
|
||||
self.size = module.params['size']
|
||||
self.type = module.params['type']
|
||||
self.comment = module.params['comment']
|
||||
self.passphrase = module.params['passphrase']
|
||||
self.regenerate = module.params['regenerate']
|
||||
self.comment = self.module.params['comment']
|
||||
self.private_key_path = self.module.params['path']
|
||||
self.public_key_path = self.private_key_path + '.pub'
|
||||
self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always'
|
||||
self.state = self.module.params['state']
|
||||
self.type = self.module.params['type']
|
||||
|
||||
self.changed = False
|
||||
self.fingerprint = ''
|
||||
self.public_key = {}
|
||||
self.size = self._get_size(self.module.params['size'])
|
||||
self._validate_path()
|
||||
|
||||
if self.regenerate == 'always':
|
||||
self.force = True
|
||||
self.original_private_key = None
|
||||
self.original_public_key = None
|
||||
self.private_key = None
|
||||
self.public_key = None
|
||||
|
||||
def _get_size(self, size):
|
||||
if self.type in ('rsa', 'rsa1'):
|
||||
self.size = 4096 if self.size is None else self.size
|
||||
if self.size < 1024:
|
||||
module.fail_json(msg=('For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. '
|
||||
'Attempting to use bit lengths under 1024 will cause the module to fail.'))
|
||||
result = 4096 if size is None else size
|
||||
if result < 1024:
|
||||
return self.module.fail_json(
|
||||
msg="For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. " +
|
||||
"Attempting to use bit lengths under 1024 will cause the module to fail."
|
||||
)
|
||||
elif self.type == 'dsa':
|
||||
self.size = 1024 if self.size is None else self.size
|
||||
if self.size != 1024:
|
||||
module.fail_json(msg=('DSA keys must be exactly 1024 bits as specified by FIPS 186-2.'))
|
||||
result = 1024 if size is None else size
|
||||
if result != 1024:
|
||||
return self.module.fail_json(msg="DSA keys must be exactly 1024 bits as specified by FIPS 186-2.")
|
||||
elif self.type == 'ecdsa':
|
||||
self.size = 256 if self.size is None else self.size
|
||||
if self.size not in (256, 384, 521):
|
||||
module.fail_json(msg=('For ECDSA keys, size determines the key length by selecting from '
|
||||
'one of three elliptic curve sizes: 256, 384 or 521 bits. '
|
||||
'Attempting to use bit lengths other than these three values for '
|
||||
'ECDSA keys will cause this module to fail. '))
|
||||
result = 256 if size is None else size
|
||||
if result not in (256, 384, 521):
|
||||
return self.module.fail_json(
|
||||
msg="For ECDSA keys, size determines the key length by selecting from one of " +
|
||||
"three elliptic curve sizes: 256, 384 or 521 bits. " +
|
||||
"Attempting to use bit lengths other than these three values for ECDSA keys will " +
|
||||
"cause this module to fail."
|
||||
)
|
||||
elif self.type == 'ed25519':
|
||||
# User input is ignored for `key size` when `key type` is ed25519
|
||||
self.size = 256
|
||||
result = 256
|
||||
else:
|
||||
module.fail_json(msg="%s is not a valid value for key type" % self.type)
|
||||
return self.module.fail_json(msg="%s is not a valid value for key type" % self.type)
|
||||
|
||||
def generate(self):
|
||||
if self.force or not self.is_private_key_valid(perms_required=False):
|
||||
try:
|
||||
if self.exists() and not os.access(self.path, os.W_OK):
|
||||
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
|
||||
self._generate_keypair()
|
||||
self.changed = True
|
||||
except (IOError, OSError) as e:
|
||||
self.remove()
|
||||
self.module.fail_json(msg="%s" % to_native(e))
|
||||
return result
|
||||
|
||||
self.fingerprint = self._get_current_key_properties()[2]
|
||||
self.public_key = self._get_public_key()
|
||||
elif not self.is_public_key_valid(perms_required=False):
|
||||
pubkey = self._get_public_key()
|
||||
try:
|
||||
with open(self.path + ".pub", "w") as pubkey_f:
|
||||
pubkey_f.write(pubkey + '\n')
|
||||
os.chmod(self.path + ".pub", stat.S_IWUSR + stat.S_IRUSR + stat.S_IRGRP + stat.S_IROTH)
|
||||
except (IOError, OSError):
|
||||
self.module.fail_json(
|
||||
msg='The public key is missing or does not match the private key. '
|
||||
'Unable to regenerate the public key.')
|
||||
self.changed = True
|
||||
self.public_key = pubkey
|
||||
def _validate_path(self):
|
||||
self._check_if_base_dir(self.private_key_path)
|
||||
|
||||
if self.comment:
|
||||
try:
|
||||
if self.exists() and not os.access(self.path, os.W_OK):
|
||||
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
|
||||
except (IOError, OSError):
|
||||
self.module.fail_json(msg='Unable to update the comment for the public key.')
|
||||
self._update_comment()
|
||||
if os.path.isdir(self.private_key_path):
|
||||
self.module.fail_json(msg='%s is a directory. Please specify a path to a file.' % self.private_key_path)
|
||||
|
||||
private_key_perms_changed = self._permissions_changed()
|
||||
public_key_perms_changed = self._permissions_changed(public_key=True)
|
||||
if private_key_perms_changed or public_key_perms_changed:
|
||||
self.changed = True
|
||||
def _execute(self):
|
||||
self.original_private_key = self._load_private_key()
|
||||
self.original_public_key = self._load_public_key()
|
||||
|
||||
def is_private_key_valid(self, perms_required=True):
|
||||
if not self.exists():
|
||||
return False
|
||||
if self.state == 'present':
|
||||
self._validate_key_load()
|
||||
|
||||
if self._check_pass_protected_or_broken_key():
|
||||
if self.regenerate in ('full_idempotence', 'always'):
|
||||
return False
|
||||
self.module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `full_idempotence` or `always`, or with `force=yes`.')
|
||||
if self._should_generate():
|
||||
self._generate()
|
||||
elif not self._public_key_valid():
|
||||
self._restore_public_key()
|
||||
|
||||
if not self._private_key_loadable():
|
||||
if os.path.isdir(self.path):
|
||||
self.module.fail_json(msg='%s is a directory. Please specify a path to a file.' % self.path)
|
||||
self.private_key = self._load_private_key()
|
||||
self.public_key = self._load_public_key()
|
||||
|
||||
if self.regenerate in ('full_idempotence', 'always'):
|
||||
return False
|
||||
self.module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.'
|
||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
||||
' set to `full_idempotence` or `always`, or with `force=yes`.')
|
||||
|
||||
keysize, keytype, self.fingerprint = self._get_current_key_properties()
|
||||
|
||||
if self.regenerate == 'never':
|
||||
return True
|
||||
|
||||
if not (self.type == keytype and self.size == keysize):
|
||||
if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'):
|
||||
return False
|
||||
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`.'
|
||||
)
|
||||
|
||||
# Perms required short-circuits evaluation to prevent the side-effects of running _permissions_changed
|
||||
# when check_mode is not enabled
|
||||
return not (perms_required and self._permissions_changed())
|
||||
|
||||
def is_public_key_valid(self, perms_required=True):
|
||||
|
||||
def _get_pubkey_content():
|
||||
if self.exists(public_key=True):
|
||||
with open(self.path + ".pub", "r") as pubkey_f:
|
||||
present_pubkey = pubkey_f.read().strip(' \n')
|
||||
return present_pubkey
|
||||
else:
|
||||
return ''
|
||||
|
||||
def _parse_pubkey(pubkey_content):
|
||||
if pubkey_content:
|
||||
parts = pubkey_content.split(' ', 2)
|
||||
if len(parts) < 2:
|
||||
return ()
|
||||
return parts[0], parts[1], '' if len(parts) <= 2 else parts[2]
|
||||
return ()
|
||||
|
||||
def _pubkey_valid(pubkey):
|
||||
if pubkey_parts and _parse_pubkey(pubkey):
|
||||
return pubkey_parts[:2] == _parse_pubkey(pubkey)[:2]
|
||||
return False
|
||||
|
||||
def _comment_valid():
|
||||
if pubkey_parts:
|
||||
return pubkey_parts[2] == self.comment
|
||||
return False
|
||||
|
||||
pubkey_parts = _parse_pubkey(_get_pubkey_content())
|
||||
|
||||
pubkey = self._get_public_key()
|
||||
if _pubkey_valid(pubkey):
|
||||
self.public_key = pubkey
|
||||
for path in (self.private_key_path, self.public_key_path):
|
||||
self._update_permissions(path)
|
||||
else:
|
||||
return False
|
||||
if self._should_remove():
|
||||
self._remove()
|
||||
|
||||
if self.comment and not _comment_valid():
|
||||
return False
|
||||
|
||||
# Perms required short-circuits evaluation to prevent the side-effects of running _permissions_changes
|
||||
# when check_mode is not enabled
|
||||
return not (perms_required and self._permissions_changed(public_key=True))
|
||||
|
||||
def _permissions_changed(self, public_key=False):
|
||||
file_args = self.module.load_file_common_arguments(self.module.params)
|
||||
if public_key:
|
||||
file_args['path'] = file_args['path'] + '.pub'
|
||||
if self.module.check_file_absent_if_check_mode(file_args['path']):
|
||||
return True
|
||||
return self.module.set_fs_attributes_if_different(file_args, False)
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
return {
|
||||
'changed': self.changed,
|
||||
'size': self.size,
|
||||
'type': self.type,
|
||||
'filename': self.path,
|
||||
'fingerprint': self.fingerprint if self.fingerprint else '',
|
||||
'public_key': self.public_key,
|
||||
'comment': self.comment if self.comment else '',
|
||||
}
|
||||
|
||||
def remove(self):
|
||||
"""Remove the resource from the filesystem."""
|
||||
|
||||
try:
|
||||
os.remove(self.path)
|
||||
self.changed = True
|
||||
except (IOError, OSError) as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
self.module.fail_json(msg=to_native(exc))
|
||||
else:
|
||||
def _load_private_key(self):
|
||||
result = None
|
||||
if self._private_key_exists():
|
||||
try:
|
||||
result = self._get_private_key()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.exists(public_key=True):
|
||||
return result
|
||||
|
||||
def _private_key_exists(self):
|
||||
return os.path.exists(self.private_key_path)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_private_key(self):
|
||||
pass
|
||||
|
||||
def _load_public_key(self):
|
||||
result = None
|
||||
if self._public_key_exists():
|
||||
try:
|
||||
os.remove(self.path + ".pub")
|
||||
self.changed = True
|
||||
except (IOError, OSError) as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
self.module.fail_json(msg=to_native(exc))
|
||||
else:
|
||||
pass
|
||||
result = PublicKey.load(self.public_key_path)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
return result
|
||||
|
||||
def exists(self, public_key=False):
|
||||
return os.path.exists(self.path if not public_key else self.path + ".pub")
|
||||
def _public_key_exists(self):
|
||||
return os.path.exists(self.public_key_path)
|
||||
|
||||
def _validate_key_load(self):
|
||||
if (self._private_key_exists()
|
||||
and self.regenerate in ('never', 'fail', 'partial_idempotence')
|
||||
and (self.original_private_key is None or not self._private_key_readable())):
|
||||
self.module.fail_json(
|
||||
msg="Unable to read the key. The key is protected with a passphrase or broken. " +
|
||||
"Will not proceed. To force regeneration, call the module with `generate` " +
|
||||
"set to `full_idempotence` or `always`, or with `force=yes`."
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _generate_keypair(self):
|
||||
def _private_key_readable(self):
|
||||
pass
|
||||
|
||||
def _should_generate(self):
|
||||
if self.regenerate == 'never':
|
||||
return self.original_private_key is None
|
||||
elif self.regenerate == 'fail':
|
||||
if not self._private_key_valid():
|
||||
self.module.fail_json(
|
||||
msg="Key has wrong type and/or size. Will not proceed. " +
|
||||
"To force regeneration, call the module with `generate` set to " +
|
||||
"`partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`."
|
||||
)
|
||||
return self.original_private_key is None
|
||||
elif self.regenerate in ('partial_idempotence', 'full_idempotence'):
|
||||
return not self._private_key_valid()
|
||||
else:
|
||||
return True
|
||||
|
||||
def _private_key_valid(self):
|
||||
if self.original_private_key is None:
|
||||
return False
|
||||
|
||||
return all([
|
||||
self.size == self.original_private_key.size,
|
||||
self.type == self.original_private_key.type,
|
||||
])
|
||||
|
||||
@OpensshModule.trigger_change
|
||||
@OpensshModule.skip_if_check_mode
|
||||
def _generate(self):
|
||||
temp_private_key, temp_public_key = self._generate_temp_keypair()
|
||||
|
||||
try:
|
||||
self._safe_secure_move([(temp_private_key, self.private_key_path), (temp_public_key, self.public_key_path)])
|
||||
except OSError as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
def _generate_temp_keypair(self):
|
||||
temp_private_key = os.path.join(self.module.tmpdir, os.path.basename(self.private_key_path))
|
||||
temp_public_key = temp_private_key + '.pub'
|
||||
|
||||
try:
|
||||
self._generate_keypair(temp_private_key)
|
||||
except (IOError, OSError) as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
for f in (temp_private_key, temp_public_key):
|
||||
self.module.add_cleanup_file(f)
|
||||
|
||||
return temp_private_key, temp_public_key
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_current_key_properties(self):
|
||||
def _generate_keypair(self, private_key_path):
|
||||
pass
|
||||
|
||||
def _public_key_valid(self):
|
||||
if self.original_public_key is None:
|
||||
return False
|
||||
|
||||
valid_public_key = self._get_public_key()
|
||||
valid_public_key.comment = self.comment
|
||||
|
||||
return self.original_public_key == valid_public_key
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key(self):
|
||||
pass
|
||||
|
||||
@OpensshModule.trigger_change
|
||||
@OpensshModule.skip_if_check_mode
|
||||
def _restore_public_key(self):
|
||||
try:
|
||||
temp_public_key = self._create_temp_public_key(str(self._get_public_key()) + '\n')
|
||||
self._safe_secure_move([
|
||||
(temp_public_key, self.public_key_path)
|
||||
])
|
||||
except (IOError, OSError):
|
||||
self.module.fail_json(
|
||||
msg="The public key is missing or does not match the private key. " +
|
||||
"Unable to regenerate the public key."
|
||||
)
|
||||
|
||||
if self.comment:
|
||||
self._update_comment()
|
||||
|
||||
def _create_temp_public_key(self, content):
|
||||
temp_public_key = os.path.join(self.module.tmpdir, os.path.basename(self.public_key_path))
|
||||
|
||||
default_permissions = 0o644
|
||||
existing_permissions = file_mode(self.public_key_path)
|
||||
|
||||
try:
|
||||
secure_write(temp_public_key, existing_permissions or default_permissions, to_bytes(content))
|
||||
except (IOError, OSError) as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
self.module.add_cleanup_file(temp_public_key)
|
||||
|
||||
return temp_public_key
|
||||
|
||||
@abc.abstractmethod
|
||||
def _update_comment(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _private_key_loadable(self):
|
||||
pass
|
||||
def _should_remove(self):
|
||||
return self._private_key_exists() or self._public_key_exists()
|
||||
|
||||
@abc.abstractmethod
|
||||
def _check_pass_protected_or_broken_key(self):
|
||||
pass
|
||||
@OpensshModule.trigger_change
|
||||
@OpensshModule.skip_if_check_mode
|
||||
def _remove(self):
|
||||
try:
|
||||
if self._private_key_exists():
|
||||
os.remove(self.private_key_path)
|
||||
if self._public_key_exists():
|
||||
os.remove(self.public_key_path)
|
||||
except (IOError, OSError) as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
@property
|
||||
def _result(self):
|
||||
private_key = self.private_key or self.original_private_key
|
||||
public_key = self.public_key or self.original_public_key
|
||||
|
||||
return {
|
||||
'size': self.size,
|
||||
'type': self.type,
|
||||
'filename': self.private_key_path,
|
||||
'fingerprint': private_key.fingerprint if private_key else '',
|
||||
'public_key': str(public_key) if public_key else '',
|
||||
'comment': public_key.comment if public_key else '',
|
||||
}
|
||||
|
||||
@property
|
||||
def diff(self):
|
||||
before = self.original_private_key.to_dict() if self.original_private_key else {}
|
||||
before.update(self.original_public_key.to_dict() if self.original_public_key else {})
|
||||
|
||||
after = self.private_key.to_dict() if self.private_key else {}
|
||||
after.update(self.public_key.to_dict() if self.public_key else {})
|
||||
|
||||
return {
|
||||
'before': before,
|
||||
'after': after,
|
||||
}
|
||||
|
||||
|
||||
class KeypairBackendOpensshBin(KeypairBackend):
|
||||
|
||||
def __init__(self, module):
|
||||
super(KeypairBackendOpensshBin, self).__init__(module)
|
||||
|
||||
self.openssh_bin = module.get_bin_path('ssh-keygen')
|
||||
self.ssh_keygen = KeygenCommand(self.module)
|
||||
|
||||
def _load_privatekey(self):
|
||||
return self.module.run_command([self.openssh_bin, '-lf', self.path])
|
||||
def _generate_keypair(self, private_key_path):
|
||||
self.ssh_keygen.generate_keypair(private_key_path, self.size, self.type, self.comment)
|
||||
|
||||
def _get_publickey_from_privatekey(self):
|
||||
# -P '' is always included as an option to induce the expected standard output for
|
||||
# _check_pass_protected_or_broken_key, but introduces no side-effects when used to
|
||||
# output a matching public key
|
||||
return self.module.run_command([self.openssh_bin, '-P', '', '-yf', self.path])
|
||||
|
||||
def _generate_keypair(self):
|
||||
args = [
|
||||
self.openssh_bin,
|
||||
'-q',
|
||||
'-N', '',
|
||||
'-b', str(self.size),
|
||||
'-t', self.type,
|
||||
'-f', self.path,
|
||||
'-C', self.comment if self.comment else ''
|
||||
]
|
||||
|
||||
# "y" must be entered in response to the "overwrite" prompt
|
||||
stdin_data = 'y' if self.exists() else None
|
||||
|
||||
self.module.run_command(args, data=stdin_data)
|
||||
|
||||
def _get_current_key_properties(self):
|
||||
rc, stdout, stderr = self._load_privatekey()
|
||||
properties = stdout.split()
|
||||
keysize = int(properties[0])
|
||||
fingerprint = properties[1]
|
||||
keytype = properties[-1][1:-1].lower()
|
||||
|
||||
return keysize, keytype, fingerprint
|
||||
def _get_private_key(self):
|
||||
private_key_content = self.ssh_keygen.get_private_key(self.private_key_path)[1]
|
||||
return PrivateKey.from_string(private_key_content)
|
||||
|
||||
def _get_public_key(self):
|
||||
rc, stdout, stderr = self._get_publickey_from_privatekey()
|
||||
return stdout.strip('\n')
|
||||
public_key_content = self.ssh_keygen.get_matching_public_key(self.private_key_path)[1]
|
||||
return PublicKey.from_string(public_key_content)
|
||||
|
||||
def _private_key_readable(self):
|
||||
rc, stdout, stderr = self.ssh_keygen.get_matching_public_key(self.private_key_path)
|
||||
return not (rc == 255 or any_in(stderr, 'is not a public key file', 'incorrect passphrase', 'load failed'))
|
||||
|
||||
def _update_comment(self):
|
||||
return self.module.run_command([self.openssh_bin, '-q', '-o', '-c', '-C', self.comment, '-f', self.path])
|
||||
|
||||
def _private_key_loadable(self):
|
||||
rc, stdout, stderr = self._load_privatekey()
|
||||
return rc == 0
|
||||
|
||||
def _check_pass_protected_or_broken_key(self):
|
||||
rc, stdout, stderr = self._get_publickey_from_privatekey()
|
||||
return rc == 255 or any_in(stderr, 'is not a public key file', 'incorrect passphrase', 'load failed')
|
||||
try:
|
||||
self.ssh_keygen.update_comment(self.private_key_path, self.comment)
|
||||
except (IOError, OSError) as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
|
||||
class KeypairBackendCryptography(KeypairBackend):
|
||||
|
||||
def __init__(self, module):
|
||||
super(KeypairBackendCryptography, self).__init__(module)
|
||||
|
||||
if module.params['private_key_format'] == 'auto':
|
||||
ssh = module.get_bin_path('ssh')
|
||||
if ssh:
|
||||
proc = module.run_command([ssh, '-Vq'])
|
||||
ssh_version = parse_openssh_version(proc[2].strip())
|
||||
else:
|
||||
# Default to OpenSSH 7.8 compatibility when OpenSSH is not installed
|
||||
ssh_version = "7.8"
|
||||
if self.type == 'rsa1':
|
||||
self.module.fail_json(msg="RSA1 keys are not supported by the cryptography backend")
|
||||
|
||||
self.private_key_format = 'SSH'
|
||||
self.passphrase = to_bytes(module.params['passphrase']) if module.params['passphrase'] else None
|
||||
self.private_key_format = self._get_key_format(module.params['private_key_format'])
|
||||
|
||||
def _get_key_format(self, key_format):
|
||||
result = 'SSH'
|
||||
|
||||
if key_format == 'auto':
|
||||
# Default to OpenSSH 7.8 compatibility when OpenSSH is not installed
|
||||
ssh_version = self._get_ssh_version() or "7.8"
|
||||
|
||||
if LooseVersion(ssh_version) < LooseVersion("7.8") and self.type != 'ed25519':
|
||||
# OpenSSH made SSH formatted private keys available in version 6.5,
|
||||
# but still defaulted to PKCS1 format with the exception of ed25519 keys
|
||||
self.private_key_format = 'PKCS1'
|
||||
result = 'PKCS1'
|
||||
|
||||
if self.private_key_format == 'SSH' and not HAS_OPENSSH_PRIVATE_FORMAT:
|
||||
module.fail_json(
|
||||
if result == 'SSH' and not HAS_OPENSSH_PRIVATE_FORMAT:
|
||||
self.module.fail_json(
|
||||
msg=missing_required_lib(
|
||||
'cryptography >= 3.0',
|
||||
reason="to load/dump private keys in the default OpenSSH format for OpenSSH >= 7.8 " +
|
||||
@@ -362,96 +372,72 @@ class KeypairBackendCryptography(KeypairBackend):
|
||||
)
|
||||
)
|
||||
|
||||
if self.type == 'rsa1':
|
||||
module.fail_json(msg="RSA1 keys are not supported by the cryptography backend")
|
||||
return result
|
||||
|
||||
self.passphrase = to_bytes(self.passphrase) if self.passphrase else None
|
||||
|
||||
def _load_privatekey(self):
|
||||
return OpensshKeypair.load(path=self.path, passphrase=self.passphrase, no_public_key=True)
|
||||
|
||||
def _generate_keypair(self):
|
||||
def _generate_keypair(self, private_key_path):
|
||||
keypair = OpensshKeypair.generate(
|
||||
keytype=self.type,
|
||||
size=self.size,
|
||||
passphrase=self.passphrase,
|
||||
comment=self.comment if self.comment else "",
|
||||
comment=self.comment or '',
|
||||
)
|
||||
with open(self.path, 'w+b') as f:
|
||||
f.write(
|
||||
OpensshKeypair.encode_openssh_privatekey(
|
||||
keypair.asymmetric_keypair,
|
||||
self.private_key_format
|
||||
)
|
||||
)
|
||||
# ssh-keygen defaults private key permissions to 0600 octal
|
||||
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
|
||||
with open(self.path + '.pub', 'w+b') as f:
|
||||
f.write(keypair.public_key)
|
||||
# ssh-keygen defaults public key permissions to 0644 octal
|
||||
os.chmod(self.path + ".pub", stat.S_IWUSR + stat.S_IRUSR + stat.S_IRGRP + stat.S_IROTH)
|
||||
|
||||
def _get_current_key_properties(self):
|
||||
keypair = self._load_privatekey()
|
||||
encoded_private_key = OpensshKeypair.encode_openssh_privatekey(
|
||||
keypair.asymmetric_keypair, self.private_key_format
|
||||
)
|
||||
secure_write(private_key_path, 0o600, encoded_private_key)
|
||||
|
||||
return keypair.size, keypair.key_type, keypair.fingerprint
|
||||
public_key_path = private_key_path + '.pub'
|
||||
secure_write(public_key_path, 0o644, keypair.public_key)
|
||||
|
||||
def _get_private_key(self):
|
||||
keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
|
||||
|
||||
return PrivateKey(
|
||||
size=keypair.size,
|
||||
key_type=keypair.key_type,
|
||||
fingerprint=keypair.fingerprint,
|
||||
)
|
||||
|
||||
def _get_public_key(self):
|
||||
try:
|
||||
keypair = self._load_privatekey()
|
||||
keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
|
||||
except OpenSSHError:
|
||||
# Simulates the null output of ssh-keygen
|
||||
return ""
|
||||
|
||||
return to_text(keypair.public_key)
|
||||
return PublicKey.from_string(to_text(keypair.public_key))
|
||||
|
||||
def _update_comment(self):
|
||||
keypair = self._load_privatekey()
|
||||
def _private_key_readable(self):
|
||||
try:
|
||||
keypair.comment = self.comment
|
||||
with open(self.path + ".pub", "w+b") as pubkey_file:
|
||||
pubkey_file.write(keypair.public_key + b'\n')
|
||||
except (InvalidCommentError, IOError, OSError) as e:
|
||||
# Return values while unused currently are made to simulate the output of run_command()
|
||||
return 1, "Comment could not be updated", to_native(e)
|
||||
return 0, "Comment updated successfully", ""
|
||||
|
||||
def _private_key_loadable(self):
|
||||
try:
|
||||
self._load_privatekey()
|
||||
except OpenSSHError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _check_pass_protected_or_broken_key(self):
|
||||
try:
|
||||
OpensshKeypair.load(
|
||||
path=self.path,
|
||||
passphrase=self.passphrase,
|
||||
no_public_key=True,
|
||||
)
|
||||
OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
|
||||
except (InvalidPrivateKeyFileError, InvalidPassphraseError):
|
||||
return True
|
||||
return False
|
||||
|
||||
# Cryptography >= 3.0 uses a SSH key loader which does not raise an exception when a passphrase is provided
|
||||
# when loading an unencrypted key
|
||||
if self.passphrase:
|
||||
try:
|
||||
OpensshKeypair.load(
|
||||
path=self.path,
|
||||
passphrase=None,
|
||||
no_public_key=True,
|
||||
)
|
||||
OpensshKeypair.load(path=self.private_key_path, passphrase=None, no_public_key=True)
|
||||
except (InvalidPrivateKeyFileError, InvalidPassphraseError):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return False
|
||||
return True
|
||||
|
||||
def _update_comment(self):
|
||||
keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
|
||||
try:
|
||||
keypair.comment = self.comment
|
||||
except InvalidCommentError as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
def any_in(sequence, *elements):
|
||||
return any([e in sequence for e in elements])
|
||||
try:
|
||||
temp_public_key = self._create_temp_public_key(keypair.public_key + b'\n')
|
||||
self._safe_secure_move([(temp_public_key, self.public_key_path)])
|
||||
except (IOError, OSError) as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
|
||||
def select_backend(module, backend):
|
||||
|
||||
@@ -555,6 +555,11 @@ class OpensshCertificate(object):
|
||||
def signing_key(self):
|
||||
return to_text(self._cert_info.signing_key_fingerprint())
|
||||
|
||||
@property
|
||||
def signature_type(self):
|
||||
signature_data = OpensshParser.signature_data(self.signature)
|
||||
return to_text(signature_data['signature_type'])
|
||||
|
||||
@staticmethod
|
||||
def _parse_cert_info(pub_key_type, parser):
|
||||
cert_info = get_cert_info_object(pub_key_type)
|
||||
|
||||
@@ -20,10 +20,11 @@ __metaclass__ = type
|
||||
|
||||
import os
|
||||
from base64 import b64encode, b64decode
|
||||
from distutils.version import LooseVersion
|
||||
from getpass import getuser
|
||||
from socket import gethostname
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
try:
|
||||
from cryptography import __version__ as CRYPTOGRAPHY_VERSION
|
||||
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from struct import Struct
|
||||
|
||||
from ansible.module_utils.six import PY3
|
||||
@@ -54,6 +56,16 @@ _UINT64 = Struct(b'!Q')
|
||||
_UINT64_MAX = 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
|
||||
def any_in(sequence, *elements):
|
||||
return any(e in sequence for e in elements)
|
||||
|
||||
|
||||
def file_mode(path):
|
||||
if not os.path.exists(path):
|
||||
return 0o000
|
||||
return os.stat(path).st_mode & 0o777
|
||||
|
||||
|
||||
def parse_openssh_version(version_string):
|
||||
"""Parse the version output of ssh -V and return version numbers that can be compared"""
|
||||
|
||||
@@ -68,6 +80,20 @@ def parse_openssh_version(version_string):
|
||||
return version
|
||||
|
||||
|
||||
@contextmanager
|
||||
def secure_open(path, mode):
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
|
||||
try:
|
||||
yield fd
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def secure_write(path, mode, content):
|
||||
with secure_open(path, mode) as fd:
|
||||
os.write(fd, content)
|
||||
|
||||
|
||||
# See https://datatracker.ietf.org/doc/html/rfc4251#section-5 for SSH data types
|
||||
class OpensshParser(object):
|
||||
"""Parser for OpenSSH encoded objects"""
|
||||
@@ -175,8 +201,9 @@ class OpensshParser(object):
|
||||
signature_blob = parser.string()
|
||||
|
||||
blob_parser = cls(signature_blob)
|
||||
if signature_type == b'ssh-rsa':
|
||||
if signature_type in (b'ssh-rsa', b'rsa-sha2-256', b'rsa-sha2-512'):
|
||||
# https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
# https://datatracker.ietf.org/doc/html/rfc8332#section-3
|
||||
signature_data['s'] = cls._big_int(signature_blob, "big")
|
||||
elif signature_type == b'ssh-dss':
|
||||
# https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
|
||||
17
plugins/module_utils/version.py
Normal file
17
plugins/module_utils/version.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2021, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
"""Provide version object to compare version numbers."""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
# Once we drop support for Ansible 2.9, ansible-base 2.10, and ansible-core 2.11, we can
|
||||
# remove the _version.py file, and replace the following import by
|
||||
#
|
||||
# from ansible.module_utils.compat.version import LooseVersion
|
||||
|
||||
from ._version import LooseVersion
|
||||
@@ -54,7 +54,7 @@ EXAMPLES = '''
|
||||
- name: Check whether an account with the given account key exists
|
||||
community.crypto.acme_account_info:
|
||||
account_key_src: /etc/pki/cert/private/account.key
|
||||
register: account_data
|
||||
register: account_data
|
||||
- name: Verify that account exists
|
||||
assert:
|
||||
that:
|
||||
@@ -70,7 +70,7 @@ EXAMPLES = '''
|
||||
acme_account_info:
|
||||
account_key_content: "{{ acme_account_key }}"
|
||||
account_uri: "{{ acme_account_uri }}"
|
||||
register: account_data
|
||||
register: account_data
|
||||
- name: Verify that account exists
|
||||
assert:
|
||||
that:
|
||||
|
||||
@@ -145,6 +145,8 @@ import traceback
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.io import (
|
||||
@@ -164,7 +166,6 @@ try:
|
||||
import cryptography.x509
|
||||
import cryptography.x509.oid
|
||||
import ipaddress
|
||||
from distutils.version import LooseVersion
|
||||
HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.3'))
|
||||
_cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||
except ImportError as dummy:
|
||||
@@ -202,7 +203,10 @@ def main():
|
||||
),
|
||||
)
|
||||
if not HAS_CRYPTOGRAPHY:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= 1.3'), exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
# Some callbacks die when exception is provided with value None
|
||||
if CRYPTOGRAPHY_IMP_ERR:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= 1.3'), exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= 1.3'))
|
||||
|
||||
try:
|
||||
# Get parameters
|
||||
|
||||
@@ -124,6 +124,8 @@ import traceback
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||
split_pem_list,
|
||||
)
|
||||
@@ -140,7 +142,6 @@ try:
|
||||
import cryptography.hazmat.primitives.asymmetric.utils
|
||||
import cryptography.x509
|
||||
import cryptography.x509.oid
|
||||
from distutils.version import LooseVersion
|
||||
HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.5'))
|
||||
_cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||
except ImportError as dummy:
|
||||
@@ -237,12 +238,14 @@ class CertificateSet(object):
|
||||
self.module = module
|
||||
self.certificates = set()
|
||||
self.certificate_by_issuer = dict()
|
||||
self.certificate_by_cert = dict()
|
||||
|
||||
def _load_file(self, path):
|
||||
certs = load_PEM_list(self.module, path, fail_on_error=False)
|
||||
for cert in certs:
|
||||
self.certificates.add(cert)
|
||||
self.certificate_by_issuer[cert.cert.subject] = cert
|
||||
self.certificate_by_cert[cert.cert] = cert
|
||||
|
||||
def load(self, path):
|
||||
'''
|
||||
@@ -274,6 +277,16 @@ def format_cert(cert):
|
||||
return str(cert.cert)
|
||||
|
||||
|
||||
def check_cycle(module, occured_certificates, next):
|
||||
'''
|
||||
Make sure that next is not in occured_certificates so far, and add it.
|
||||
'''
|
||||
next_cert = next.cert
|
||||
if next_cert in occured_certificates:
|
||||
module.fail_json(msg='Found cycle while building certificate chain')
|
||||
occured_certificates.add(next_cert)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
@@ -312,13 +325,19 @@ def main():
|
||||
# Try to complete chain
|
||||
current = chain[-1]
|
||||
completed = []
|
||||
occured_certificates = set([cert.cert for cert in chain])
|
||||
if current.cert in roots.certificate_by_cert:
|
||||
# Don't try to complete the chain when it's already ending with a root certificate
|
||||
current = None
|
||||
while current:
|
||||
root = roots.find_parent(current)
|
||||
if root:
|
||||
check_cycle(module, occured_certificates, root)
|
||||
completed.append(root)
|
||||
break
|
||||
intermediate = intermediates.find_parent(current)
|
||||
if intermediate:
|
||||
check_cycle(module, occured_certificates, intermediate)
|
||||
completed.append(intermediate)
|
||||
current = intermediate
|
||||
else:
|
||||
|
||||
@@ -519,11 +519,11 @@ import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
write_file,
|
||||
)
|
||||
|
||||
@@ -50,6 +50,14 @@ options:
|
||||
- Proxy port used when get a certificate.
|
||||
type: int
|
||||
default: 8080
|
||||
starttls:
|
||||
description:
|
||||
- Requests a secure connection for protocols which require clients to initiate encryption.
|
||||
- Only available for C(mysql) currently.
|
||||
type: str
|
||||
choices:
|
||||
- mysql
|
||||
version_added: 1.9.0
|
||||
timeout:
|
||||
description:
|
||||
- The timeout in seconds
|
||||
@@ -159,7 +167,6 @@ import base64
|
||||
import datetime
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from os.path import isfile
|
||||
from socket import create_connection, setdefaulttimeout, socket
|
||||
from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_REQUIRED
|
||||
@@ -167,6 +174,8 @@ from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_RE
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_oid_to_name,
|
||||
cryptography_get_extensions_from_cert,
|
||||
@@ -209,6 +218,20 @@ else:
|
||||
CRYPTOGRAPHY_FOUND = True
|
||||
|
||||
|
||||
def send_starttls_packet(sock, server_type):
|
||||
if server_type == 'mysql':
|
||||
ssl_request_packet = (
|
||||
b'\x20\x00\x00\x01\x85\xae\x7f\x00' +
|
||||
b'\x00\x00\x00\x01\x21\x00\x00\x00' +
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00' +
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00' +
|
||||
b'\x00\x00\x00\x00'
|
||||
)
|
||||
|
||||
sock.recv(8192) # discard initial handshake from server for this naive implementation
|
||||
sock.send(ssl_request_packet)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
@@ -220,6 +243,7 @@ def main():
|
||||
server_name=dict(type='str'),
|
||||
timeout=dict(type='int', default=10),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
starttls=dict(type='str', choices=['mysql']),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -230,6 +254,7 @@ def main():
|
||||
proxy_port = module.params.get('proxy_port')
|
||||
timeout = module.params.get('timeout')
|
||||
server_name = module.params.get('server_name')
|
||||
start_tls_server_type = module.params.get('starttls')
|
||||
|
||||
backend = module.params.get('select_crypto_backend')
|
||||
if backend == 'auto':
|
||||
@@ -305,6 +330,9 @@ def main():
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = CERT_NONE
|
||||
|
||||
if start_tls_server_type is not None:
|
||||
send_starttls_packet(sock, start_tls_server_type)
|
||||
|
||||
cert = ctx.wrap_socket(sock, server_hostname=server_name or host).getpeercert(True)
|
||||
cert = DER_cert_to_PEM_cert(cert)
|
||||
except Exception as e:
|
||||
|
||||
@@ -356,6 +356,34 @@ LUKS_NAME_REGEX = re.compile(r'\s*crypt\s+([^\s]*)\s*')
|
||||
LUKS_DEVICE_REGEX = re.compile(r'\s*device:\s+([^\s]*)\s*')
|
||||
|
||||
|
||||
# See https://gitlab.com/cryptsetup/cryptsetup/-/wikis/LUKS-standard/on-disk-format.pdf
|
||||
LUKS_HEADER = b'LUKS\xba\xbe'
|
||||
LUKS_HEADER_L = 6
|
||||
# See https://gitlab.com/cryptsetup/LUKS2-docs/-/blob/master/luks2_doc_wip.pdf
|
||||
LUKS2_HEADER_OFFSETS = [0x4000, 0x8000, 0x10000, 0x20000, 0x40000, 0x80000, 0x100000, 0x200000, 0x400000]
|
||||
LUKS2_HEADER2 = b'SKUL\xba\xbe'
|
||||
|
||||
|
||||
def wipe_luks_headers(device):
|
||||
wipe_offsets = []
|
||||
with open(device, 'rb') as f:
|
||||
# f.seek(0)
|
||||
data = f.read(LUKS_HEADER_L)
|
||||
if data == LUKS_HEADER:
|
||||
wipe_offsets.append(0)
|
||||
for offset in LUKS2_HEADER_OFFSETS:
|
||||
f.seek(offset)
|
||||
data = f.read(LUKS_HEADER_L)
|
||||
if data == LUKS2_HEADER2:
|
||||
wipe_offsets.append(offset)
|
||||
|
||||
if wipe_offsets:
|
||||
with open(device, 'wb') as f:
|
||||
for offset in wipe_offsets:
|
||||
f.seek(offset)
|
||||
f.write(b'\x00\x00\x00\x00\x00\x00')
|
||||
|
||||
|
||||
class Handler(object):
|
||||
|
||||
def __init__(self, module):
|
||||
@@ -515,9 +543,17 @@ class CryptHandler(Handler):
|
||||
self.run_luks_close(name)
|
||||
result = self._run_command([wipefs_bin, '--all', device])
|
||||
if result[RETURN_CODE] != 0:
|
||||
raise ValueError('Error while wiping luks container %s: %s'
|
||||
raise ValueError('Error while wiping LUKS container signatures for %s: %s'
|
||||
% (device, result[STDERR]))
|
||||
|
||||
# For LUKS2, sometimes both `cryptsetup erase` and `wipefs` do **not**
|
||||
# erase all LUKS signatures (they seem to miss the second header). That's
|
||||
# why we do it ourselves here.
|
||||
try:
|
||||
wipe_luks_headers(device)
|
||||
except Exception as exc:
|
||||
raise ValueError('Error while wiping LUKS container signatures for %s: %s' % (device, exc))
|
||||
|
||||
def run_luks_add_key(self, device, keyfile, passphrase, new_keyfile,
|
||||
new_passphrase, pbkdf):
|
||||
''' Add new key from a keyfile or passphrase to given 'device';
|
||||
|
||||
@@ -44,12 +44,12 @@ options:
|
||||
required: true
|
||||
regenerate:
|
||||
description:
|
||||
- When C(never) the task will fail if a certificate already exists at I(path) and is unreadable.
|
||||
Otherwise, a new certificate will only be generated if there is no existing certificate.
|
||||
- When C(never) the task will fail if a certificate already exists at I(path) and is unreadable
|
||||
otherwise a new certificate will only be generated if there is no existing certificate.
|
||||
- When C(fail) the task will fail if a certificate already exists at I(path) and does not
|
||||
match the module's options.
|
||||
- When C(partial_idempotence) an existing certificate will be regenerated based on
|
||||
I(serial), I(type), I(valid_from), I(valid_to), I(valid_at), and I(principals).
|
||||
I(serial), I(signature_algorithm), I(type), I(valid_from), I(valid_to), I(valid_at), and I(principals).
|
||||
- When C(full_idempotence) I(identifier), I(options), I(public_key), and I(signing_key)
|
||||
are also considered when compared against an existing certificate.
|
||||
- C(always) is equivalent to I(force=true).
|
||||
@@ -62,6 +62,26 @@ options:
|
||||
- always
|
||||
default: partial_idempotence
|
||||
version_added: 1.8.0
|
||||
signature_algorithm:
|
||||
description:
|
||||
- As of OpenSSH 8.2 the SHA-1 signature algorithm for RSA keys has been disabled and C(ssh) will refuse
|
||||
host certificates signed with the SHA-1 algorithm. OpenSSH 8.1 made C(rsa-sha2-512) the default algorithm
|
||||
when acting as a CA and signing certificates with a RSA key. However, for OpenSSH versions less than 8.1
|
||||
the SHA-2 signature algorithms, C(rsa-sha2-256) or C(rsa-sha2-512), must be specified using this option
|
||||
if compatibility with newer C(ssh) clients is required. Conversely if hosts using OpenSSH version 8.2
|
||||
or greater must remain compatible with C(ssh) clients using OpenSSH less than 7.2, then C(ssh-rsa)
|
||||
can be used when generating host certificates (a corresponding change to the sshd_config to add C(ssh-rsa)
|
||||
to the C(CASignatureAlgorithms) keyword is also required).
|
||||
- Using any value for this option with a non-RSA I(signing_key) will cause this module to fail.
|
||||
- "Note: OpenSSH versions prior to 7.2 do not support SHA-2 signature algorithms for RSA keys and OpenSSH
|
||||
versions prior to 7.3 do not support SHA-2 signature algorithms for certificates."
|
||||
- See U(https://www.openssh.com/txt/release-8.2) for more information.
|
||||
type: str
|
||||
choices:
|
||||
- ssh-rsa
|
||||
- rsa-sha2-256
|
||||
- rsa-sha2-512
|
||||
version_added: 1.10.0
|
||||
signing_key:
|
||||
description:
|
||||
- The path to the private openssh key that is used for signing the public key in order to generate the certificate.
|
||||
@@ -238,13 +258,17 @@ info:
|
||||
'''
|
||||
|
||||
import os
|
||||
from distutils.version import LooseVersion
|
||||
from sys import version_info
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import safe_atomic_move
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import (
|
||||
KeygenCommand,
|
||||
OpensshModule,
|
||||
PrivateKey,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.certificate import (
|
||||
OpensshCertificate,
|
||||
@@ -252,153 +276,122 @@ from ansible_collections.community.crypto.plugins.module_utils.openssh.certifica
|
||||
parse_option_list,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
|
||||
parse_openssh_version,
|
||||
)
|
||||
|
||||
PY27 = version_info[0:2] >= (2, 7)
|
||||
|
||||
|
||||
class Certificate(object):
|
||||
class Certificate(OpensshModule):
|
||||
def __init__(self, module):
|
||||
self.check_mode = module.check_mode
|
||||
self.module = module
|
||||
self.ssh_keygen = module.get_bin_path('ssh-keygen', True)
|
||||
super(Certificate, self).__init__(module)
|
||||
self.ssh_keygen = KeygenCommand(self.module)
|
||||
|
||||
self.force = module.params['force']
|
||||
self.identifier = module.params['identifier'] or ""
|
||||
self.options = module.params['options'] or []
|
||||
self.path = module.params['path']
|
||||
self.pkcs11_provider = module.params['pkcs11_provider']
|
||||
self.principals = module.params['principals'] or []
|
||||
self.public_key = module.params['public_key']
|
||||
self.regenerate = module.params['regenerate'] if not self.force else 'always'
|
||||
self.serial_number = module.params['serial_number']
|
||||
self.signing_key = module.params['signing_key']
|
||||
self.state = module.params['state']
|
||||
self.type = module.params['type']
|
||||
self.use_agent = module.params['use_agent']
|
||||
self.valid_at = module.params['valid_at']
|
||||
self.identifier = self.module.params['identifier'] or ""
|
||||
self.options = self.module.params['options'] or []
|
||||
self.path = self.module.params['path']
|
||||
self.pkcs11_provider = self.module.params['pkcs11_provider']
|
||||
self.principals = self.module.params['principals'] or []
|
||||
self.public_key = self.module.params['public_key']
|
||||
self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always'
|
||||
self.serial_number = self.module.params['serial_number']
|
||||
self.signature_algorithm = self.module.params['signature_algorithm']
|
||||
self.signing_key = self.module.params['signing_key']
|
||||
self.state = self.module.params['state']
|
||||
self.type = self.module.params['type']
|
||||
self.use_agent = self.module.params['use_agent']
|
||||
self.valid_at = self.module.params['valid_at']
|
||||
|
||||
self._check_if_base_dir(self.path)
|
||||
|
||||
if self.state == 'present':
|
||||
self._validate_parameters()
|
||||
|
||||
self.changed = False
|
||||
self.data = None
|
||||
self.original_data = None
|
||||
if self._exists():
|
||||
self._load_certificate()
|
||||
|
||||
self.time_parameters = None
|
||||
|
||||
if self.state == 'present':
|
||||
try:
|
||||
self.time_parameters = OpensshCertificateTimeParameters(
|
||||
valid_from=module.params['valid_from'],
|
||||
valid_to=module.params['valid_to'],
|
||||
)
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
self._set_time_parameters()
|
||||
|
||||
if self.exists():
|
||||
try:
|
||||
self.original_data = OpensshCertificate.load(self.path)
|
||||
except (TypeError, ValueError) as e:
|
||||
if self.regenerate in ('never', 'fail'):
|
||||
self.module.fail_json(msg="Unable to read existing certificate: %s" % to_native(e))
|
||||
self.module.warn("Unable to read existing certificate: %s" % to_native(e))
|
||||
def _validate_parameters(self):
|
||||
for path in (self.public_key, self.signing_key):
|
||||
self._check_if_base_dir(path)
|
||||
|
||||
self._validate_parameters()
|
||||
if self.options and self.type == "host":
|
||||
self.module.fail_json(msg="Options can only be used with user certificates.")
|
||||
|
||||
def exists(self):
|
||||
return os.path.exists(self.path)
|
||||
if self.use_agent:
|
||||
self._use_agent_available()
|
||||
|
||||
def generate(self):
|
||||
if self._should_generate():
|
||||
if not self.check_mode:
|
||||
temp_cert = self._generate_temp_certificate()
|
||||
|
||||
try:
|
||||
safe_atomic_move(self.module, temp_cert, self.path)
|
||||
except OSError as e:
|
||||
self.module.fail_json(msg="Unable to write certificate to %s: %s" % (self.path, to_native(e)))
|
||||
|
||||
try:
|
||||
self.data = OpensshCertificate.load(self.path)
|
||||
except (TypeError, ValueError) as e:
|
||||
self.module.fail_json(msg="Unable to read new certificate: %s" % to_native(e))
|
||||
|
||||
self.changed = True
|
||||
|
||||
if self.exists():
|
||||
self._update_permissions()
|
||||
|
||||
def remove(self):
|
||||
if self.exists():
|
||||
if not self.check_mode:
|
||||
try:
|
||||
os.remove(self.path)
|
||||
except OSError as e:
|
||||
self.module.fail_json(msg="Unable to remove existing certificate: %s" % to_native(e))
|
||||
self.changed = True
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
result = {'changed': self.changed}
|
||||
|
||||
if self.module._diff:
|
||||
result['diff'] = {
|
||||
'before': get_cert_dict(self.original_data),
|
||||
'after': get_cert_dict(self.data)
|
||||
}
|
||||
|
||||
if self.state == 'present':
|
||||
result.update({
|
||||
'type': self.type,
|
||||
'filename': self.path,
|
||||
'info': format_cert_info(self._get_cert_info()),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def _check_if_base_dir(self, path):
|
||||
base_dir = os.path.dirname(path) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
def _use_agent_available(self):
|
||||
ssh_version = self._get_ssh_version()
|
||||
if not ssh_version:
|
||||
self.module.fail_json(msg="Failed to determine ssh version")
|
||||
elif LooseVersion(ssh_version) < LooseVersion("7.6"):
|
||||
self.module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
msg="Signing with CA key in ssh agent requires ssh 7.6 or newer." +
|
||||
" Your version is: %s" % ssh_version
|
||||
)
|
||||
|
||||
def _command_arguments(self, key_copy_path):
|
||||
result = [
|
||||
self.ssh_keygen,
|
||||
'-s', self.signing_key,
|
||||
'-P', '',
|
||||
'-I', self.identifier,
|
||||
]
|
||||
def _exists(self):
|
||||
return os.path.exists(self.path)
|
||||
|
||||
if self.options:
|
||||
for option in self.options:
|
||||
result.extend(['-O', option])
|
||||
if self.pkcs11_provider:
|
||||
result.extend(['-D', self.pkcs11_provider])
|
||||
if self.principals:
|
||||
result.extend(['-n', ','.join(self.principals)])
|
||||
if self.serial_number is not None:
|
||||
result.extend(['-z', str(self.serial_number)])
|
||||
if self.type == 'host':
|
||||
result.extend(['-h'])
|
||||
if self.use_agent:
|
||||
result.extend(['-U'])
|
||||
if self.time_parameters.validity_string:
|
||||
result.extend(['-V', self.time_parameters.validity_string])
|
||||
result.append(key_copy_path)
|
||||
|
||||
return result
|
||||
|
||||
def _compare_options(self):
|
||||
def _load_certificate(self):
|
||||
try:
|
||||
critical_options, extensions = parse_option_list(self.options)
|
||||
except ValueError as e:
|
||||
return self.module.fail_json(msg=to_native(e))
|
||||
self.original_data = OpensshCertificate.load(self.path)
|
||||
except (TypeError, ValueError) as e:
|
||||
if self.regenerate in ('never', 'fail'):
|
||||
self.module.fail_json(msg="Unable to read existing certificate: %s" % to_native(e))
|
||||
self.module.warn("Unable to read existing certificate: %s" % to_native(e))
|
||||
|
||||
def _set_time_parameters(self):
|
||||
try:
|
||||
self.time_parameters = OpensshCertificateTimeParameters(
|
||||
valid_from=self.module.params['valid_from'],
|
||||
valid_to=self.module.params['valid_to'],
|
||||
)
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
def _execute(self):
|
||||
if self.state == 'present':
|
||||
if self._should_generate():
|
||||
self._generate()
|
||||
self._update_permissions(self.path)
|
||||
else:
|
||||
if self._exists():
|
||||
self._remove()
|
||||
|
||||
def _should_generate(self):
|
||||
if self.regenerate == 'never':
|
||||
return self.original_data is None
|
||||
elif self.regenerate == 'fail':
|
||||
if self.original_data and not self._is_fully_valid():
|
||||
self.module.fail_json(
|
||||
msg="Certificate does not match the provided options.",
|
||||
cert=get_cert_dict(self.original_data)
|
||||
)
|
||||
return self.original_data is None
|
||||
elif self.regenerate == 'partial_idempotence':
|
||||
return self.original_data is None or not self._is_partially_valid()
|
||||
elif self.regenerate == 'full_idempotence':
|
||||
return self.original_data is None or not self._is_fully_valid()
|
||||
else:
|
||||
return True
|
||||
|
||||
def _is_fully_valid(self):
|
||||
return self._is_partially_valid() and all([
|
||||
self._compare_options(),
|
||||
self.original_data.key_id == self.identifier,
|
||||
self.original_data.public_key == self._get_key_fingerprint(self.public_key),
|
||||
self.original_data.signing_key == self._get_key_fingerprint(self.signing_key),
|
||||
])
|
||||
|
||||
def _is_partially_valid(self):
|
||||
return all([
|
||||
set(self.original_data.critical_options) == set(critical_options),
|
||||
set(self.original_data.extensions) == set(extensions)
|
||||
set(self.original_data.principals) == set(self.principals),
|
||||
self.original_data.signature_type == self.signature_algorithm if self.signature_algorithm else True,
|
||||
self.original_data.serial == self.serial_number if self.serial_number is not None else True,
|
||||
self.original_data.type == self.type,
|
||||
self._compare_time_parameters(),
|
||||
])
|
||||
|
||||
def _compare_time_parameters(self):
|
||||
@@ -415,6 +408,35 @@ class Certificate(object):
|
||||
original_time_parameters.within_range(self.valid_at)
|
||||
])
|
||||
|
||||
def _compare_options(self):
|
||||
try:
|
||||
critical_options, extensions = parse_option_list(self.options)
|
||||
except ValueError as e:
|
||||
return self.module.fail_json(msg=to_native(e))
|
||||
|
||||
return all([
|
||||
set(self.original_data.critical_options) == set(critical_options),
|
||||
set(self.original_data.extensions) == set(extensions)
|
||||
])
|
||||
|
||||
def _get_key_fingerprint(self, path):
|
||||
private_key_content = self.ssh_keygen.get_private_key(path, check_rc=True)[1]
|
||||
return PrivateKey.from_string(private_key_content).fingerprint
|
||||
|
||||
@OpensshModule.trigger_change
|
||||
@OpensshModule.skip_if_check_mode
|
||||
def _generate(self):
|
||||
try:
|
||||
temp_certificate = self._generate_temp_certificate()
|
||||
self._safe_secure_move([(temp_certificate, self.path)])
|
||||
except OSError as e:
|
||||
self.module.fail_json(msg="Unable to write certificate to %s: %s" % (self.path, to_native(e)))
|
||||
|
||||
try:
|
||||
self.data = OpensshCertificate.load(self.path)
|
||||
except (TypeError, ValueError) as e:
|
||||
self.module.fail_json(msg="Unable to read new certificate: %s" % to_native(e))
|
||||
|
||||
def _generate_temp_certificate(self):
|
||||
key_copy = os.path.join(self.module.tmpdir, os.path.basename(self.public_key))
|
||||
|
||||
@@ -424,77 +446,44 @@ class Certificate(object):
|
||||
self.module.fail_json(msg="Unable to stage temporary key: %s" % to_native(e))
|
||||
self.module.add_cleanup_file(key_copy)
|
||||
|
||||
self.module.run_command(self._command_arguments(key_copy), environ_update=dict(TZ="UTC"), check_rc=True)
|
||||
self.ssh_keygen.generate_certificate(
|
||||
key_copy, self.identifier, self.options, self.pkcs11_provider, self.principals, self.serial_number,
|
||||
self.signature_algorithm, self.signing_key, self.type, self.time_parameters, self.use_agent,
|
||||
environ_update=dict(TZ="UTC"), check_rc=True
|
||||
)
|
||||
|
||||
temp_cert = os.path.splitext(key_copy)[0] + '-cert.pub'
|
||||
self.module.add_cleanup_file(temp_cert)
|
||||
|
||||
return temp_cert
|
||||
|
||||
def _get_cert_info(self):
|
||||
return self.module.run_command([self.ssh_keygen, '-Lf', self.path])[1]
|
||||
@OpensshModule.trigger_change
|
||||
@OpensshModule.skip_if_check_mode
|
||||
def _remove(self):
|
||||
try:
|
||||
os.remove(self.path)
|
||||
except OSError as e:
|
||||
self.module.fail_json(msg="Unable to remove existing certificate: %s" % to_native(e))
|
||||
|
||||
def _get_key_fingerprint(self, path):
|
||||
stdout = self.module.run_command([self.ssh_keygen, '-lf', path], check_rc=True)[1]
|
||||
return stdout.split()[1]
|
||||
@property
|
||||
def _result(self):
|
||||
if self.state != 'present':
|
||||
return {}
|
||||
|
||||
def _is_valid(self):
|
||||
partial_result = all([
|
||||
set(self.original_data.principals) == set(self.principals),
|
||||
self.original_data.serial == self.serial_number if self.serial_number is not None else True,
|
||||
self.original_data.type == self.type,
|
||||
self._compare_time_parameters(),
|
||||
])
|
||||
certificate_info = self.ssh_keygen.get_certificate_info(self.path)[1]
|
||||
|
||||
if self.regenerate == 'partial_idempotence':
|
||||
return partial_result
|
||||
return {
|
||||
'type': self.type,
|
||||
'filename': self.path,
|
||||
'info': format_cert_info(certificate_info),
|
||||
}
|
||||
|
||||
return partial_result and all([
|
||||
self._compare_options(),
|
||||
self.original_data.key_id == self.identifier,
|
||||
self.original_data.public_key == self._get_key_fingerprint(self.public_key),
|
||||
self.original_data.signing_key == self._get_key_fingerprint(self.signing_key),
|
||||
])
|
||||
|
||||
def _should_generate(self):
|
||||
if self.regenerate == 'never':
|
||||
return self.original_data is None
|
||||
elif self.regenerate == 'fail':
|
||||
if self.original_data and not self._is_valid():
|
||||
self.module.fail_json(
|
||||
msg="Certificate does not match the provided options.",
|
||||
cert=get_cert_dict(self.original_data)
|
||||
)
|
||||
return self.original_data is None
|
||||
elif self.regenerate in ('partial_idempotence', 'full_idempotence'):
|
||||
return self.original_data is None or not self._is_valid()
|
||||
else:
|
||||
return True
|
||||
|
||||
def _update_permissions(self):
|
||||
file_args = self.module.load_file_common_arguments(self.module.params)
|
||||
self.changed = self.module.set_fs_attributes_if_different(file_args, self.changed)
|
||||
|
||||
def _validate_parameters(self):
|
||||
self._check_if_base_dir(self.path)
|
||||
|
||||
if self.state == 'present':
|
||||
for path in (self.public_key, self.signing_key):
|
||||
self._check_if_base_dir(path)
|
||||
|
||||
if self.options and self.type == "host":
|
||||
self.module.fail_json(msg="Options can only be used with user certificates.")
|
||||
|
||||
if self.use_agent:
|
||||
ssh_version_string = self.module.run_command([self.module.get_bin_path('ssh', True), '-Vq'])[2].strip()
|
||||
ssh_version = parse_openssh_version(ssh_version_string)
|
||||
if ssh_version is None:
|
||||
self.module.fail_json(msg="Failed to parse ssh version from: %s" % ssh_version_string)
|
||||
elif LooseVersion(ssh_version) < LooseVersion("7.6"):
|
||||
self.module.fail_json(
|
||||
msg="Signing with CA key in ssh agent requires ssh 7.6 or newer." +
|
||||
" Your version is: %s" % ssh_version_string
|
||||
)
|
||||
@property
|
||||
def diff(self):
|
||||
return {
|
||||
'before': get_cert_dict(self.original_data),
|
||||
'after': get_cert_dict(self.data)
|
||||
}
|
||||
|
||||
|
||||
def format_cert_info(cert_info):
|
||||
@@ -519,6 +508,8 @@ def get_cert_dict(data):
|
||||
|
||||
result = data.to_dict()
|
||||
result.pop('nonce')
|
||||
result['signature_algorithm'] = data.signature_type
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -537,6 +528,7 @@ def main():
|
||||
default='partial_idempotence',
|
||||
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
|
||||
),
|
||||
signature_algorithm=dict(type='str', choices=['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512']),
|
||||
signing_key=dict(type='path'),
|
||||
serial_number=dict(type='int'),
|
||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
||||
@@ -551,14 +543,7 @@ def main():
|
||||
required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])],
|
||||
)
|
||||
|
||||
certificate = Certificate(module)
|
||||
|
||||
if certificate.state == 'present':
|
||||
certificate.generate()
|
||||
else:
|
||||
certificate.remove()
|
||||
|
||||
module.exit_json(**certificate.result)
|
||||
Certificate(module).execute()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -186,8 +186,6 @@ comment:
|
||||
sample: test@comment
|
||||
'''
|
||||
|
||||
import os
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.keypair_backend import (
|
||||
@@ -218,32 +216,9 @@ def main():
|
||||
add_file_common_args=True,
|
||||
)
|
||||
|
||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
|
||||
keypair = select_backend(module, module.params['backend'])[1]
|
||||
|
||||
if module.params['state'] == 'present':
|
||||
if module.check_mode:
|
||||
keypair.changed = any([
|
||||
keypair.force,
|
||||
not keypair.is_private_key_valid(),
|
||||
not keypair.is_public_key_valid()
|
||||
])
|
||||
else:
|
||||
keypair.generate()
|
||||
else:
|
||||
# When `state=absent` no details from an existing key at the given `path` are returned in the module result
|
||||
if module.check_mode:
|
||||
keypair.changed = keypair.exists()
|
||||
else:
|
||||
keypair.remove()
|
||||
|
||||
module.exit_json(**keypair.result)
|
||||
keypair.execute()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -128,11 +128,11 @@ import re
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
load_file_if_exists,
|
||||
write_file,
|
||||
|
||||
@@ -239,11 +239,11 @@ import os
|
||||
import stat
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
load_file_if_exists,
|
||||
write_file,
|
||||
|
||||
@@ -182,11 +182,11 @@ publickey:
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
load_file_if_exists,
|
||||
write_file,
|
||||
|
||||
@@ -61,7 +61,7 @@ EXAMPLES = r'''
|
||||
path: /etc/ssl/private/ansible.com.pem
|
||||
|
||||
- name: Create public key from private key
|
||||
community.crypto.openssl_privatekey:
|
||||
community.crypto.openssl_publickey:
|
||||
privatekey_path: /etc/ssl/private/ansible.com.pem
|
||||
path: /etc/ssl/ansible.com.pub
|
||||
|
||||
|
||||
@@ -96,9 +96,10 @@ signature:
|
||||
|
||||
import os
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
import base64
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.11'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
|
||||
|
||||
|
||||
@@ -96,9 +96,10 @@ valid:
|
||||
|
||||
import os
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
import base64
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.11'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
|
||||
|
||||
|
||||
@@ -367,11 +367,11 @@ 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.common.text.converters import to_native, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
write_file,
|
||||
)
|
||||
@@ -597,7 +597,7 @@ class CRL(OpenSSLObject):
|
||||
entry['invalidity_date_critical'],
|
||||
)
|
||||
|
||||
def check(self, perms_required=True, ignore_conversion=True):
|
||||
def check(self, module, perms_required=True, ignore_conversion=True):
|
||||
"""Ensure the resource is in its desired state."""
|
||||
|
||||
state_and_perms = super(CRL, self).check(self.module, perms_required)
|
||||
@@ -689,9 +689,9 @@ class CRL(OpenSSLObject):
|
||||
|
||||
def generate(self):
|
||||
result = None
|
||||
if not self.check(perms_required=False, ignore_conversion=True) or self.force:
|
||||
if not self.check(self.module, perms_required=False, ignore_conversion=True) or self.force:
|
||||
result = self._generate_crl()
|
||||
elif not self.check(perms_required=False, ignore_conversion=False) and self.crl:
|
||||
elif not self.check(self.module, perms_required=False, ignore_conversion=False) and self.crl:
|
||||
if self.format == 'pem':
|
||||
result = self.crl.public_bytes(Encoding.PEM)
|
||||
else:
|
||||
@@ -834,7 +834,7 @@ def main():
|
||||
if module.params['state'] == 'present':
|
||||
if module.check_mode:
|
||||
result = crl.dump(check_mode=True)
|
||||
result['changed'] = module.params['force'] or not crl.check() or not crl.check(ignore_conversion=False)
|
||||
result['changed'] = module.params['force'] or not crl.check(module) or not crl.check(module, ignore_conversion=False)
|
||||
module.exit_json(**result)
|
||||
|
||||
crl.generate()
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
shippable/cloud/group1
|
||||
cloud/acme
|
||||
|
||||
# Since skipping below fails miserably with ansible-core 2.11 and earlier, we have to skip all POSIX tests...
|
||||
# (https://github.com/ansible/ansible/issues/75711)
|
||||
# shippable/posix/group1
|
||||
|
||||
# Skip all VMs, since we cannot talk to the ACME simulator from these:
|
||||
# (TODO: remove when ansible-core 2.12 is the earliest version we support)
|
||||
# skip/aix
|
||||
# skip/freebsd
|
||||
# skip/macos
|
||||
# skip/osx
|
||||
# skip/rhel
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
dependencies:
|
||||
- setup_acme
|
||||
- setup_remote_tmp_dir
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
- block:
|
||||
- name: Generate account keys
|
||||
openssl_privatekey:
|
||||
path: "{{ output_dir }}/{{ item.name }}.pem"
|
||||
passphrase: "{{ item.pass | default(omit, true) }}"
|
||||
cipher: "{{ 'auto' if item.pass | default() else omit }}"
|
||||
path: "{{ remote_tmp_dir }}/{{ item.name }}.pem"
|
||||
passphrase: "{{ item.pass | default(omit) | default(omit, true) }}"
|
||||
cipher: "{{ 'auto' if (item.pass | default(false)) else omit }}"
|
||||
type: ECC
|
||||
curve: secp256r1
|
||||
force: true
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
- name: Parse account keys (to ease debugging some test failures)
|
||||
openssl_privatekey_info:
|
||||
path: "{{ output_dir }}/{{ item.name }}.pem"
|
||||
passphrase: "{{ item.pass | default(omit, true) }}"
|
||||
path: "{{ remote_tmp_dir }}/{{ item.name }}.pem"
|
||||
passphrase: "{{ item.pass | default(omit) | default(omit, true) }}"
|
||||
return_private_key_data: true
|
||||
loop: "{{ account_keys }}"
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
- name: Do not try to create account
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -40,7 +40,7 @@
|
||||
- name: Create it now (check mode, diff)
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -56,7 +56,7 @@
|
||||
- name: Create it now
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -70,7 +70,7 @@
|
||||
- name: Create it now (idempotent)
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -81,10 +81,15 @@
|
||||
- mailto:example@example.org
|
||||
register: account_created_idempotent
|
||||
|
||||
- name: Read account key
|
||||
slurp:
|
||||
src: '{{ remote_tmp_dir }}/accountkey.pem'
|
||||
register: slurp
|
||||
|
||||
- name: Change email address (check mode, diff)
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_content: "{{ lookup('file', output_dir ~ '/accountkey.pem') }}"
|
||||
account_key_content: "{{ slurp.content | b64decode }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -99,7 +104,7 @@
|
||||
- name: Change email address
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_content: "{{ lookup('file', output_dir ~ '/accountkey.pem') }}"
|
||||
account_key_content: "{{ slurp.content | b64decode }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -112,7 +117,7 @@
|
||||
- name: Change email address (idempotent)
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
account_uri: "{{ account_created.account_uri }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
@@ -126,7 +131,7 @@
|
||||
- name: Cannot access account with wrong URI
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
account_uri: "{{ account_created.account_uri ~ '12345thisdoesnotexist' }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
@@ -139,7 +144,7 @@
|
||||
- name: Clear contact email addresses (check mode, diff)
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -153,7 +158,7 @@
|
||||
- name: Clear contact email addresses
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -165,7 +170,7 @@
|
||||
- name: Clear contact email addresses (idempotent)
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -177,11 +182,11 @@
|
||||
- name: Change account key (check mode, diff)
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
new_account_key_src: "{{ output_dir }}/accountkey2.pem"
|
||||
new_account_key_src: "{{ remote_tmp_dir }}/accountkey2.pem"
|
||||
new_account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||
state: changed_key
|
||||
contact:
|
||||
@@ -193,11 +198,11 @@
|
||||
- name: Change account key
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
new_account_key_src: "{{ output_dir }}/accountkey2.pem"
|
||||
new_account_key_src: "{{ remote_tmp_dir }}/accountkey2.pem"
|
||||
new_account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||
state: changed_key
|
||||
contact:
|
||||
@@ -207,7 +212,7 @@
|
||||
- name: Deactivate account (check mode, diff)
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey2.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey2.pem"
|
||||
account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
@@ -220,7 +225,7 @@
|
||||
- name: Deactivate account
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey2.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey2.pem"
|
||||
account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
@@ -231,7 +236,7 @@
|
||||
- name: Deactivate account (idempotent)
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey2.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey2.pem"
|
||||
account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
@@ -242,7 +247,7 @@
|
||||
- name: Do not try to create account II
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey2.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey2.pem"
|
||||
account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
@@ -255,7 +260,7 @@
|
||||
- name: Do not try to create account III
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -267,7 +272,7 @@
|
||||
- name: Create account with External Account Binding
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/{{ item.account }}.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/{{ item.account }}.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
- name: Remove output directory
|
||||
file:
|
||||
path: "{{ output_dir }}"
|
||||
path: "{{ remote_tmp_dir }}"
|
||||
state: absent
|
||||
|
||||
- name: Re-create output directory
|
||||
file:
|
||||
path: "{{ output_dir }}"
|
||||
path: "{{ remote_tmp_dir }}"
|
||||
state: directory
|
||||
|
||||
- block:
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
shippable/cloud/group1
|
||||
cloud/acme
|
||||
|
||||
# Since skipping below fails miserably with ansible-core 2.11 and earlier, we have to skip all POSIX tests...
|
||||
# (https://github.com/ansible/ansible/issues/75711)
|
||||
# shippable/posix/group1
|
||||
|
||||
# Skip all VMs, since we cannot talk to the ACME simulator from these:
|
||||
# (TODO: remove when ansible-core 2.12 is the earliest version we support)
|
||||
# skip/aix
|
||||
# skip/freebsd
|
||||
# skip/macos
|
||||
# skip/osx
|
||||
# skip/rhel
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
dependencies:
|
||||
- setup_acme
|
||||
- setup_remote_tmp_dir
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- block:
|
||||
- name: Generate account keys
|
||||
openssl_privatekey:
|
||||
path: "{{ output_dir }}/{{ item }}.pem"
|
||||
path: "{{ remote_tmp_dir }}/{{ item }}.pem"
|
||||
type: ECC
|
||||
curve: secp256r1
|
||||
force: true
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
- name: Parse account keys (to ease debugging some test failures)
|
||||
openssl_privatekey_info:
|
||||
path: "{{ output_dir }}/{{ item }}.pem"
|
||||
path: "{{ remote_tmp_dir }}/{{ item }}.pem"
|
||||
return_private_key_data: true
|
||||
loop: "{{ account_keys }}"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
- name: Check that account does not exist
|
||||
acme_account_info:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -31,7 +31,7 @@
|
||||
- name: Create it now
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -44,16 +44,21 @@
|
||||
- name: Check that account exists
|
||||
acme_account_info:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
register: account_created
|
||||
|
||||
- name: Read account key
|
||||
slurp:
|
||||
src: '{{ remote_tmp_dir }}/accountkey.pem'
|
||||
register: slurp
|
||||
|
||||
- name: Clear email address
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_content: "{{ lookup('file', output_dir ~ '/accountkey.pem') }}"
|
||||
account_key_content: "{{ slurp.content | b64decode }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -64,7 +69,7 @@
|
||||
- name: Check that account was modified
|
||||
acme_account_info:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -74,7 +79,7 @@
|
||||
- name: Check with wrong account URI
|
||||
acme_account_info:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -84,7 +89,7 @@
|
||||
- name: Check with wrong account key
|
||||
acme_account_info:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/accountkey2.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey2.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
- name: Remove output directory
|
||||
file:
|
||||
path: "{{ output_dir }}"
|
||||
path: "{{ remote_tmp_dir }}"
|
||||
state: absent
|
||||
|
||||
- name: Re-create output directory
|
||||
file:
|
||||
path: "{{ output_dir }}"
|
||||
path: "{{ remote_tmp_dir }}"
|
||||
state: directory
|
||||
|
||||
- block:
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
shippable/cloud/group1
|
||||
cloud/acme
|
||||
|
||||
# Since skipping below fails miserably with ansible-core 2.11 and earlier, we have to skip all POSIX tests...
|
||||
# (https://github.com/ansible/ansible/issues/75711)
|
||||
# shippable/posix/group1
|
||||
|
||||
# Skip all VMs, since we cannot talk to the ACME simulator from these:
|
||||
# (TODO: remove when ansible-core 2.12 is the earliest version we support)
|
||||
# skip/aix
|
||||
# skip/freebsd
|
||||
# skip/macos
|
||||
# skip/osx
|
||||
# skip/rhel
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
dependencies:
|
||||
- setup_acme
|
||||
- setup_pyopenssl # needed for Ubuntu 16.04
|
||||
- setup_remote_tmp_dir
|
||||
- prepare_jinja2_compat
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- block:
|
||||
- name: Generate account keys
|
||||
openssl_privatekey:
|
||||
path: "{{ output_dir }}/{{ item.name }}.pem"
|
||||
path: "{{ remote_tmp_dir }}/{{ item.name }}.pem"
|
||||
type: "{{ item.type }}"
|
||||
size: "{{ item.size | default(omit) }}"
|
||||
curve: "{{ item.curve | default(omit) }}"
|
||||
@@ -28,15 +28,19 @@
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
|
||||
state: absent
|
||||
- name: Read account key (EC384)
|
||||
slurp:
|
||||
src: '{{ remote_tmp_dir }}/account-ec384.pem'
|
||||
register: slurp
|
||||
- name: Create ECC384 account
|
||||
acme_account:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec384.pem') }}"
|
||||
account_key_content: "{{ slurp.content | b64decode }}"
|
||||
state: present
|
||||
allow_creation: yes
|
||||
terms_agreed: yes
|
||||
@@ -49,7 +53,7 @@
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
account_key_src: "{{ output_dir }}/account-rsa.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/account-rsa.pem"
|
||||
state: present
|
||||
allow_creation: yes
|
||||
terms_agreed: yes
|
||||
@@ -115,6 +119,10 @@
|
||||
set_fact:
|
||||
cert_2_obtain_results: "{{ certificate_obtain_result }}"
|
||||
cert_2_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
||||
- name: Read account key (RSA)
|
||||
slurp:
|
||||
src: '{{ remote_tmp_dir }}/account-rsa.pem'
|
||||
register: slurp_account_key
|
||||
- name: Obtain cert 3
|
||||
include_tasks: obtain-cert.yml
|
||||
vars:
|
||||
@@ -123,7 +131,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-rsa.pem') }}"
|
||||
account_key_content: "{{ slurp_account_key.content | b64decode }}"
|
||||
challenge: dns-01
|
||||
modify_account: no
|
||||
deactivate_authzs: no
|
||||
@@ -231,6 +239,10 @@
|
||||
set_fact:
|
||||
cert_5_recreate_2: "{{ challenge_data is changed }}"
|
||||
cert_5c_obtain_results: "{{ certificate_obtain_result }}"
|
||||
- name: Read account key (EC384)
|
||||
slurp:
|
||||
src: '{{ remote_tmp_dir }}/account-ec384.pem'
|
||||
register: slurp_account_key
|
||||
- name: Obtain cert 5 (should again by force)
|
||||
include_tasks: obtain-cert.yml
|
||||
vars:
|
||||
@@ -239,7 +251,7 @@
|
||||
key_type: ec521
|
||||
subject_alt_name: "DNS:t2.example.com"
|
||||
subject_alt_name_critical: no
|
||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec384.pem') }}"
|
||||
account_key_content: "{{ slurp_account_key.content | b64decode }}"
|
||||
challenge: http-01
|
||||
modify_account: no
|
||||
deactivate_authzs: yes
|
||||
@@ -252,189 +264,204 @@
|
||||
set_fact:
|
||||
cert_5_recreate_3: "{{ challenge_data is changed }}"
|
||||
cert_5d_obtain_results: "{{ certificate_obtain_result }}"
|
||||
- name: Obtain cert 6
|
||||
include_tasks: obtain-cert.yml
|
||||
vars:
|
||||
certgen_title: Certificate 6
|
||||
certificate_name: cert-6
|
||||
key_type: rsa
|
||||
rsa_bits: "{{ default_rsa_key_size }}"
|
||||
subject_alt_name: "DNS:example.org"
|
||||
subject_alt_name_critical: no
|
||||
account_key: account-ec256
|
||||
challenge: tls-alpn-01
|
||||
modify_account: yes
|
||||
deactivate_authzs: no
|
||||
force: no
|
||||
remaining_days: 10
|
||||
terms_agreed: yes
|
||||
account_email: "example@example.org"
|
||||
acme_expected_root_number: 0
|
||||
select_chain:
|
||||
# All intermediates have the same subject key identifier, so always
|
||||
# the first chain will be found, and we need a second condition to
|
||||
# make sure that the first condition actually works. (The second
|
||||
# condition has been tested above.)
|
||||
- test_certificates: first
|
||||
subject_key_identifier: "{{ acme_intermediates[0].subject_key_identifier }}"
|
||||
- test_certificates: last
|
||||
issuer: "{{ acme_roots[1].subject }}"
|
||||
use_csr_content: true
|
||||
- name: Store obtain results for cert 6
|
||||
set_fact:
|
||||
cert_6_obtain_results: "{{ certificate_obtain_result }}"
|
||||
cert_6_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
||||
- name: Obtain cert 7
|
||||
include_tasks: obtain-cert.yml
|
||||
vars:
|
||||
certgen_title: Certificate 7
|
||||
certificate_name: cert-7
|
||||
key_type: rsa
|
||||
rsa_bits: "{{ default_rsa_key_size }}"
|
||||
subject_alt_name:
|
||||
- "IP:127.0.0.1"
|
||||
# - "IP:::1"
|
||||
subject_alt_name_critical: no
|
||||
account_key: account-ec256
|
||||
challenge: http-01
|
||||
modify_account: yes
|
||||
deactivate_authzs: no
|
||||
force: no
|
||||
remaining_days: 10
|
||||
terms_agreed: yes
|
||||
account_email: "example@example.org"
|
||||
acme_expected_root_number: 2
|
||||
select_chain:
|
||||
- test_certificates: last
|
||||
authority_key_identifier: "{{ acme_roots[2].subject_key_identifier }}"
|
||||
use_csr_content: false
|
||||
- name: Store obtain results for cert 7
|
||||
set_fact:
|
||||
cert_7_obtain_results: "{{ certificate_obtain_result }}"
|
||||
cert_7_alternate: "{{ 2 if select_crypto_backend == 'cryptography' else 0 }}"
|
||||
- name: Obtain cert 8
|
||||
include_tasks: obtain-cert.yml
|
||||
vars:
|
||||
certgen_title: Certificate 8
|
||||
certificate_name: cert-8
|
||||
key_type: rsa
|
||||
rsa_bits: "{{ default_rsa_key_size }}"
|
||||
subject_alt_name:
|
||||
- "IP:127.0.0.1"
|
||||
# IPv4 only since our test validation server doesn't work
|
||||
# with IPv6 (thanks to Python's socketserver).
|
||||
subject_alt_name_critical: no
|
||||
account_key: account-ec256
|
||||
challenge: tls-alpn-01
|
||||
challenge_alpn_tls: acme_challenge_cert_helper
|
||||
modify_account: yes
|
||||
deactivate_authzs: no
|
||||
force: no
|
||||
remaining_days: 10
|
||||
terms_agreed: yes
|
||||
account_email: "example@example.org"
|
||||
use_csr_content: true
|
||||
- name: Store obtain results for cert 8
|
||||
set_fact:
|
||||
cert_8_obtain_results: "{{ certificate_obtain_result }}"
|
||||
cert_8_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
||||
- block:
|
||||
- name: Obtain cert 6
|
||||
include_tasks: obtain-cert.yml
|
||||
vars:
|
||||
certgen_title: Certificate 6
|
||||
certificate_name: cert-6
|
||||
key_type: rsa
|
||||
rsa_bits: "{{ default_rsa_key_size }}"
|
||||
subject_alt_name: "DNS:example.org"
|
||||
subject_alt_name_critical: no
|
||||
account_key: account-ec256
|
||||
challenge: tls-alpn-01
|
||||
modify_account: yes
|
||||
deactivate_authzs: no
|
||||
force: no
|
||||
remaining_days: 10
|
||||
terms_agreed: yes
|
||||
account_email: "example@example.org"
|
||||
acme_expected_root_number: 0
|
||||
select_chain:
|
||||
# All intermediates have the same subject key identifier, so always
|
||||
# the first chain will be found, and we need a second condition to
|
||||
# make sure that the first condition actually works. (The second
|
||||
# condition has been tested above.)
|
||||
- test_certificates: first
|
||||
subject_key_identifier: "{{ acme_intermediates[0].subject_key_identifier }}"
|
||||
- test_certificates: last
|
||||
issuer: "{{ acme_roots[1].subject }}"
|
||||
use_csr_content: true
|
||||
- name: Store obtain results for cert 6
|
||||
set_fact:
|
||||
cert_6_obtain_results: "{{ certificate_obtain_result }}"
|
||||
cert_6_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
||||
when: acme_intermediates[0].subject_key_identifier is defined
|
||||
- block:
|
||||
- name: Obtain cert 7
|
||||
include_tasks: obtain-cert.yml
|
||||
vars:
|
||||
certgen_title: Certificate 7
|
||||
certificate_name: cert-7
|
||||
key_type: rsa
|
||||
rsa_bits: "{{ default_rsa_key_size }}"
|
||||
subject_alt_name:
|
||||
- "IP:127.0.0.1"
|
||||
# - "IP:::1"
|
||||
subject_alt_name_critical: no
|
||||
account_key: account-ec256
|
||||
challenge: http-01
|
||||
modify_account: yes
|
||||
deactivate_authzs: no
|
||||
force: no
|
||||
remaining_days: 10
|
||||
terms_agreed: yes
|
||||
account_email: "example@example.org"
|
||||
acme_expected_root_number: 2
|
||||
select_chain:
|
||||
- test_certificates: last
|
||||
authority_key_identifier: "{{ acme_roots[2].subject_key_identifier }}"
|
||||
use_csr_content: false
|
||||
- name: Store obtain results for cert 7
|
||||
set_fact:
|
||||
cert_7_obtain_results: "{{ certificate_obtain_result }}"
|
||||
cert_7_alternate: "{{ 2 if select_crypto_backend == 'cryptography' else 0 }}"
|
||||
when: acme_roots[2].subject_key_identifier is defined
|
||||
- block:
|
||||
- name: Obtain cert 8
|
||||
include_tasks: obtain-cert.yml
|
||||
vars:
|
||||
certgen_title: Certificate 8
|
||||
certificate_name: cert-8
|
||||
key_type: rsa
|
||||
rsa_bits: "{{ default_rsa_key_size }}"
|
||||
subject_alt_name:
|
||||
- "IP:127.0.0.1"
|
||||
# IPv4 only since our test validation server doesn't work
|
||||
# with IPv6 (thanks to Python's socketserver).
|
||||
subject_alt_name_critical: no
|
||||
account_key: account-ec256
|
||||
challenge: tls-alpn-01
|
||||
challenge_alpn_tls: acme_challenge_cert_helper
|
||||
modify_account: yes
|
||||
deactivate_authzs: no
|
||||
force: no
|
||||
remaining_days: 10
|
||||
terms_agreed: yes
|
||||
account_email: "example@example.org"
|
||||
use_csr_content: true
|
||||
- name: Store obtain results for cert 8
|
||||
set_fact:
|
||||
cert_8_obtain_results: "{{ certificate_obtain_result }}"
|
||||
cert_8_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
||||
when: cryptography_version.stdout is version('1.3', '>=')
|
||||
## DISSECT CERTIFICATES #######################################################################
|
||||
# Make sure certificates are valid. Root certificate for Pebble equals the chain certificate.
|
||||
- name: Verifying cert 1
|
||||
command: '{{ openssl_binary }} verify -CAfile "{{ output_dir }}/cert-1-root.pem" -untrusted "{{ output_dir }}/cert-1-chain.pem" "{{ output_dir }}/cert-1.pem"'
|
||||
command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-1-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-1-chain.pem" "{{ remote_tmp_dir }}/cert-1.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_1_valid
|
||||
- name: Verifying cert 2
|
||||
command: '{{ openssl_binary }} 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 "{{ remote_tmp_dir }}/cert-2-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-2-chain.pem" "{{ remote_tmp_dir }}/cert-2.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_2_valid
|
||||
- name: Verifying cert 3
|
||||
command: '{{ openssl_binary }} 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 "{{ remote_tmp_dir }}/cert-3-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-3-chain.pem" "{{ remote_tmp_dir }}/cert-3.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_3_valid
|
||||
- name: Verifying cert 4
|
||||
command: '{{ openssl_binary }} 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 "{{ remote_tmp_dir }}/cert-4-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-4-chain.pem" "{{ remote_tmp_dir }}/cert-4.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_4_valid
|
||||
- name: Verifying cert 5
|
||||
command: '{{ openssl_binary }} 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 "{{ remote_tmp_dir }}/cert-5-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-5-chain.pem" "{{ remote_tmp_dir }}/cert-5.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_5_valid
|
||||
- name: Verifying cert 6
|
||||
command: '{{ openssl_binary }} 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 "{{ remote_tmp_dir }}/cert-6-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-6-chain.pem" "{{ remote_tmp_dir }}/cert-6.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_6_valid
|
||||
when: acme_intermediates[0].subject_key_identifier is defined
|
||||
- name: Verifying cert 7
|
||||
command: '{{ openssl_binary }} 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 "{{ remote_tmp_dir }}/cert-7-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-7-chain.pem" "{{ remote_tmp_dir }}/cert-7.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_7_valid
|
||||
when: acme_roots[2].subject_key_identifier is defined
|
||||
- name: Verifying cert 8
|
||||
command: '{{ openssl_binary }} 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 "{{ remote_tmp_dir }}/cert-8-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-8-chain.pem" "{{ remote_tmp_dir }}/cert-8.pem"'
|
||||
ignore_errors: yes
|
||||
register: cert_8_valid
|
||||
when: cryptography_version.stdout is version('1.3', '>=')
|
||||
# Dump certificate info
|
||||
- name: Dumping cert 1
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-1.pem" -noout -text'
|
||||
command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-1.pem" -noout -text'
|
||||
register: cert_1_text
|
||||
- name: Dumping cert 2
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-2.pem" -noout -text'
|
||||
command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-2.pem" -noout -text'
|
||||
register: cert_2_text
|
||||
- name: Dumping cert 3
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-3.pem" -noout -text'
|
||||
command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-3.pem" -noout -text'
|
||||
register: cert_3_text
|
||||
- name: Dumping cert 4
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-4.pem" -noout -text'
|
||||
command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-4.pem" -noout -text'
|
||||
register: cert_4_text
|
||||
- name: Dumping cert 5
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-5.pem" -noout -text'
|
||||
command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-5.pem" -noout -text'
|
||||
register: cert_5_text
|
||||
- name: Dumping cert 6
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-6.pem" -noout -text'
|
||||
command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-6.pem" -noout -text'
|
||||
register: cert_6_text
|
||||
when: acme_intermediates[0].subject_key_identifier is defined
|
||||
- name: Dumping cert 7
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-7.pem" -noout -text'
|
||||
command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-7.pem" -noout -text'
|
||||
register: cert_7_text
|
||||
when: acme_roots[2].subject_key_identifier is defined
|
||||
- name: Dumping cert 8
|
||||
command: '{{ openssl_binary }} x509 -in "{{ output_dir }}/cert-8.pem" -noout -text'
|
||||
command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-8.pem" -noout -text'
|
||||
register: cert_8_text
|
||||
when: cryptography_version.stdout is version('1.3', '>=')
|
||||
# Dump certificate info
|
||||
- name: Dumping cert 1
|
||||
x509_certificate_info:
|
||||
path: "{{ output_dir }}/cert-1.pem"
|
||||
path: "{{ remote_tmp_dir }}/cert-1.pem"
|
||||
register: cert_1_info
|
||||
- name: Dumping cert 2
|
||||
x509_certificate_info:
|
||||
path: "{{ output_dir }}/cert-2.pem"
|
||||
path: "{{ remote_tmp_dir }}/cert-2.pem"
|
||||
register: cert_2_info
|
||||
- name: Dumping cert 3
|
||||
x509_certificate_info:
|
||||
path: "{{ output_dir }}/cert-3.pem"
|
||||
path: "{{ remote_tmp_dir }}/cert-3.pem"
|
||||
register: cert_3_info
|
||||
- name: Dumping cert 4
|
||||
x509_certificate_info:
|
||||
path: "{{ output_dir }}/cert-4.pem"
|
||||
path: "{{ remote_tmp_dir }}/cert-4.pem"
|
||||
register: cert_4_info
|
||||
- name: Dumping cert 5
|
||||
x509_certificate_info:
|
||||
path: "{{ output_dir }}/cert-5.pem"
|
||||
path: "{{ remote_tmp_dir }}/cert-5.pem"
|
||||
register: cert_5_info
|
||||
- name: Dumping cert 6
|
||||
x509_certificate_info:
|
||||
path: "{{ output_dir }}/cert-6.pem"
|
||||
path: "{{ remote_tmp_dir }}/cert-6.pem"
|
||||
register: cert_6_info
|
||||
when: acme_intermediates[0].subject_key_identifier is defined
|
||||
- name: Dumping cert 7
|
||||
x509_certificate_info:
|
||||
path: "{{ output_dir }}/cert-7.pem"
|
||||
path: "{{ remote_tmp_dir }}/cert-7.pem"
|
||||
register: cert_7_info
|
||||
when: acme_roots[2].subject_key_identifier is defined
|
||||
- name: Dumping cert 8
|
||||
x509_certificate_info:
|
||||
path: "{{ output_dir }}/cert-8.pem"
|
||||
path: "{{ remote_tmp_dir }}/cert-8.pem"
|
||||
register: cert_8_info
|
||||
when: cryptography_version.stdout is version('1.3', '>=')
|
||||
## GET ACCOUNT ORDERS #########################################################################
|
||||
- name: Don't retrieve orders
|
||||
acme_account_info:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -443,7 +470,7 @@
|
||||
- name: Retrieve orders as URL list (1/2)
|
||||
acme_account_info:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -452,7 +479,7 @@
|
||||
- name: Retrieve orders as URL list (2/2)
|
||||
acme_account_info:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/account-ec384.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/account-ec384.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -461,7 +488,7 @@
|
||||
- name: Retrieve orders as object list (1/2)
|
||||
acme_account_info:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -470,7 +497,7 @@
|
||||
- name: Retrieve orders as object list (2/2)
|
||||
acme_account_info:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/account-ec384.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/account-ec384.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
|
||||
@@ -8,38 +8,48 @@
|
||||
- name: Obtain root and intermediate certificates
|
||||
get_url:
|
||||
url: "http://{{ acme_host }}:5000/{{ item.0 }}-certificate-for-ca/{{ item.1 }}"
|
||||
dest: "{{ output_dir }}/acme-{{ item.0 }}-{{ item.1 }}.pem"
|
||||
dest: "{{ remote_tmp_dir }}/acme-{{ item.0 }}-{{ item.1 }}.pem"
|
||||
loop: "{{ query('nested', types, root_numbers) }}"
|
||||
|
||||
- name: Analyze root certificates
|
||||
x509_certificate_info:
|
||||
path: "{{ output_dir }}/acme-root-{{ item }}.pem"
|
||||
path: "{{ remote_tmp_dir }}/acme-root-{{ item }}.pem"
|
||||
loop: "{{ root_numbers }}"
|
||||
register: acme_roots
|
||||
|
||||
- name: Analyze intermediate certificates
|
||||
x509_certificate_info:
|
||||
path: "{{ output_dir }}/acme-intermediate-{{ item }}.pem"
|
||||
path: "{{ remote_tmp_dir }}/acme-intermediate-{{ item }}.pem"
|
||||
loop: "{{ root_numbers }}"
|
||||
register: acme_intermediates
|
||||
|
||||
- set_fact:
|
||||
x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}"
|
||||
y__: "{{ lookup('file', output_dir ~ '/acme-root-' ~ item.item ~ '.pem', rstrip=False) }}"
|
||||
loop: "{{ acme_roots.results }}"
|
||||
register: acme_roots_tmp
|
||||
- name: Read root certificates
|
||||
slurp:
|
||||
src: "{{ remote_tmp_dir ~ '/acme-root-' ~ item ~ '.pem' }}"
|
||||
loop: "{{ root_numbers }}"
|
||||
register: slurp_roots
|
||||
|
||||
- set_fact:
|
||||
x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}"
|
||||
loop: "{{ acme_roots.results }}"
|
||||
register: acme_roots_tmp
|
||||
|
||||
- name: Read intermediate certificates
|
||||
slurp:
|
||||
src: "{{ remote_tmp_dir ~ '/acme-intermediate-' ~ item ~ '.pem' }}"
|
||||
loop: "{{ root_numbers }}"
|
||||
register: slurp_intermediates
|
||||
|
||||
- set_fact:
|
||||
x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}"
|
||||
y__: "{{ lookup('file', output_dir ~ '/acme-intermediate-' ~ item.item ~ '.pem', rstrip=False) }}"
|
||||
loop: "{{ acme_intermediates.results }}"
|
||||
register: acme_intermediates_tmp
|
||||
|
||||
- set_fact:
|
||||
acme_roots: "{{ acme_roots_tmp.results | map(attribute='ansible_facts.x__') | list }}"
|
||||
acme_root_certs: "{{ acme_roots_tmp.results | map(attribute='ansible_facts.y__') | list }}"
|
||||
acme_root_certs: "{{ slurp_roots.results | map(attribute='content') | map('b64decode') | list }}"
|
||||
acme_intermediates: "{{ acme_intermediates_tmp.results | map(attribute='ansible_facts.x__') | list }}"
|
||||
acme_intermediate_certs: "{{ acme_intermediates_tmp.results | map(attribute='ansible_facts.y__') | list }}"
|
||||
acme_intermediate_certs: "{{ slurp_intermediates.results | map(attribute='content') | map('b64decode') | list }}"
|
||||
|
||||
vars:
|
||||
types:
|
||||
@@ -88,12 +98,12 @@
|
||||
|
||||
- name: Remove output directory
|
||||
file:
|
||||
path: "{{ output_dir }}"
|
||||
path: "{{ remote_tmp_dir }}"
|
||||
state: absent
|
||||
|
||||
- name: Re-create output directory
|
||||
file:
|
||||
path: "{{ output_dir }}"
|
||||
path: "{{ remote_tmp_dir }}"
|
||||
state: directory
|
||||
|
||||
- block:
|
||||
|
||||
@@ -7,6 +7,14 @@
|
||||
assert:
|
||||
that:
|
||||
- "'DNS:example.com' in cert_1_text.stdout"
|
||||
- name: Read certificate 1 files
|
||||
slurp:
|
||||
src: '{{ remote_tmp_dir }}/{{ item }}'
|
||||
loop:
|
||||
- cert-1.pem
|
||||
- cert-1-chain.pem
|
||||
- cert-1-fullchain.pem
|
||||
register: slurp
|
||||
- name: Check that certificate 1 retrieval got all chains
|
||||
assert:
|
||||
that:
|
||||
@@ -15,9 +23,9 @@
|
||||
- "'cert' in cert_1_obtain_results.all_chains[cert_1_alternate | int]"
|
||||
- "'chain' in cert_1_obtain_results.all_chains[cert_1_alternate | int]"
|
||||
- "'full_chain' in cert_1_obtain_results.all_chains[cert_1_alternate | int]"
|
||||
- "lookup('file', output_dir ~ '/cert-1.pem', rstrip=False) == cert_1_obtain_results.all_chains[cert_1_alternate | int].cert"
|
||||
- "lookup('file', output_dir ~ '/cert-1-chain.pem', rstrip=False) == cert_1_obtain_results.all_chains[cert_1_alternate | int].chain"
|
||||
- "lookup('file', output_dir ~ '/cert-1-fullchain.pem', rstrip=False) == cert_1_obtain_results.all_chains[cert_1_alternate | int].full_chain"
|
||||
- "(slurp.results[0].content | b64decode) == cert_1_obtain_results.all_chains[cert_1_alternate | int].cert"
|
||||
- "(slurp.results[1].content | b64decode) == cert_1_obtain_results.all_chains[cert_1_alternate | int].chain"
|
||||
- "(slurp.results[2].content | b64decode) == cert_1_obtain_results.all_chains[cert_1_alternate | int].full_chain"
|
||||
|
||||
- name: Check that certificate 2 is valid
|
||||
assert:
|
||||
@@ -28,6 +36,14 @@
|
||||
that:
|
||||
- "'DNS:*.example.com' in cert_2_text.stdout"
|
||||
- "'DNS:example.com' in cert_2_text.stdout"
|
||||
- name: Read certificate 2 files
|
||||
slurp:
|
||||
src: '{{ remote_tmp_dir }}/{{ item }}'
|
||||
loop:
|
||||
- cert-2.pem
|
||||
- cert-2-chain.pem
|
||||
- cert-2-fullchain.pem
|
||||
register: slurp
|
||||
- name: Check that certificate 1 retrieval got all chains
|
||||
assert:
|
||||
that:
|
||||
@@ -36,9 +52,9 @@
|
||||
- "'cert' in cert_2_obtain_results.all_chains[cert_2_alternate | int]"
|
||||
- "'chain' in cert_2_obtain_results.all_chains[cert_2_alternate | int]"
|
||||
- "'full_chain' in cert_2_obtain_results.all_chains[cert_2_alternate | int]"
|
||||
- "lookup('file', output_dir ~ '/cert-2.pem', rstrip=False) == cert_2_obtain_results.all_chains[cert_2_alternate | int].cert"
|
||||
- "lookup('file', output_dir ~ '/cert-2-chain.pem', rstrip=False) == cert_2_obtain_results.all_chains[cert_2_alternate | int].chain"
|
||||
- "lookup('file', output_dir ~ '/cert-2-fullchain.pem', rstrip=False) == cert_2_obtain_results.all_chains[cert_2_alternate | int].full_chain"
|
||||
- "(slurp.results[0].content | b64decode) == cert_2_obtain_results.all_chains[cert_2_alternate | int].cert"
|
||||
- "(slurp.results[1].content | b64decode) == cert_2_obtain_results.all_chains[cert_2_alternate | int].chain"
|
||||
- "(slurp.results[2].content | b64decode) == cert_2_obtain_results.all_chains[cert_2_alternate | int].full_chain"
|
||||
|
||||
- name: Check that certificate 3 is valid
|
||||
assert:
|
||||
@@ -50,6 +66,14 @@
|
||||
- "'DNS:*.example.com' in cert_3_text.stdout"
|
||||
- "'DNS:example.org' in cert_3_text.stdout"
|
||||
- "'DNS:t1.example.com' in cert_3_text.stdout"
|
||||
- name: Read certificate 3 files
|
||||
slurp:
|
||||
src: '{{ remote_tmp_dir }}/{{ item }}'
|
||||
loop:
|
||||
- cert-3.pem
|
||||
- cert-3-chain.pem
|
||||
- cert-3-fullchain.pem
|
||||
register: slurp
|
||||
- name: Check that certificate 1 retrieval got all chains
|
||||
assert:
|
||||
that:
|
||||
@@ -58,9 +82,9 @@
|
||||
- "'cert' in cert_3_obtain_results.all_chains[cert_3_alternate | int]"
|
||||
- "'chain' in cert_3_obtain_results.all_chains[cert_3_alternate | int]"
|
||||
- "'full_chain' in cert_3_obtain_results.all_chains[cert_3_alternate | int]"
|
||||
- "lookup('file', output_dir ~ '/cert-3.pem', rstrip=False) == cert_3_obtain_results.all_chains[cert_3_alternate | int].cert"
|
||||
- "lookup('file', output_dir ~ '/cert-3-chain.pem', rstrip=False) == cert_3_obtain_results.all_chains[cert_3_alternate | int].chain"
|
||||
- "lookup('file', output_dir ~ '/cert-3-fullchain.pem', rstrip=False) == cert_3_obtain_results.all_chains[cert_3_alternate | int].full_chain"
|
||||
- "(slurp.results[0].content | b64decode) == cert_3_obtain_results.all_chains[cert_3_alternate | int].cert"
|
||||
- "(slurp.results[1].content | b64decode) == cert_3_obtain_results.all_chains[cert_3_alternate | int].chain"
|
||||
- "(slurp.results[2].content | b64decode) == cert_3_obtain_results.all_chains[cert_3_alternate | int].full_chain"
|
||||
|
||||
- name: Check that certificate 4 is valid
|
||||
assert:
|
||||
@@ -100,14 +124,38 @@
|
||||
that:
|
||||
- cert_5_recreate_3 == True
|
||||
|
||||
- name: Check that certificate 6 is valid
|
||||
assert:
|
||||
that:
|
||||
- cert_6_valid is not failed
|
||||
- name: Check that certificate 6 contains correct SANs
|
||||
assert:
|
||||
that:
|
||||
- "'DNS:example.org' in cert_6_text.stdout"
|
||||
- block:
|
||||
- name: Check that certificate 6 is valid
|
||||
assert:
|
||||
that:
|
||||
- cert_6_valid is not failed
|
||||
- name: Check that certificate 6 contains correct SANs
|
||||
assert:
|
||||
that:
|
||||
- "'DNS:example.org' in cert_6_text.stdout"
|
||||
when: acme_intermediates[0].subject_key_identifier is defined
|
||||
|
||||
- block:
|
||||
- name: Check that certificate 7 is valid
|
||||
assert:
|
||||
that:
|
||||
- cert_7_valid is not failed
|
||||
- name: Check that certificate 7 contains correct SANs
|
||||
assert:
|
||||
that:
|
||||
- "'IP Address:127.0.0.1' in cert_8_text.stdout or 'IP:127.0.0.1' in cert_8_text.stdout"
|
||||
when: acme_roots[2].subject_key_identifier is defined
|
||||
|
||||
- block:
|
||||
- name: Check that certificate 8 is valid
|
||||
assert:
|
||||
that:
|
||||
- cert_8_valid is not failed
|
||||
- name: Check that certificate 8 contains correct SANs
|
||||
assert:
|
||||
that:
|
||||
- "'IP Address:127.0.0.1' in cert_8_text.stdout or 'IP:127.0.0.1' in cert_8_text.stdout"
|
||||
when: cryptography_version.stdout is version('1.3', '>=')
|
||||
|
||||
- name: Validate that orders were not retrieved
|
||||
assert:
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
shippable/cloud/group1
|
||||
cloud/acme
|
||||
|
||||
# Since skipping below fails miserably with ansible-core 2.11 and earlier, we have to skip all POSIX tests...
|
||||
# (https://github.com/ansible/ansible/issues/75711)
|
||||
# shippable/posix/group1
|
||||
|
||||
# Skip all VMs, since we cannot talk to the ACME simulator from these:
|
||||
# (TODO: remove when ansible-core 2.12 is the earliest version we support)
|
||||
# skip/aix
|
||||
# skip/freebsd
|
||||
# skip/macos
|
||||
# skip/osx
|
||||
# skip/rhel
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
dependencies:
|
||||
- setup_acme
|
||||
- setup_remote_tmp_dir
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- block:
|
||||
- name: Generate account keys
|
||||
openssl_privatekey:
|
||||
path: "{{ output_dir }}/{{ item.name }}.pem"
|
||||
path: "{{ remote_tmp_dir }}/{{ item.name }}.pem"
|
||||
type: "{{ item.type }}"
|
||||
size: "{{ item.size | default(omit) }}"
|
||||
curve: "{{ item.curve | default(omit) }}"
|
||||
@@ -22,6 +22,10 @@
|
||||
type: RSA
|
||||
size: "{{ default_rsa_key_size }}"
|
||||
## CREATE ACCOUNTS AND OBTAIN CERTIFICATES ####################################################
|
||||
- name: Read account key (EC256)
|
||||
slurp:
|
||||
src: '{{ remote_tmp_dir }}/account-ec256.pem'
|
||||
register: slurp_account_key
|
||||
- name: Obtain cert 1
|
||||
include_tasks: obtain-cert.yml
|
||||
vars:
|
||||
@@ -31,7 +35,7 @@
|
||||
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') }}"
|
||||
account_key_content: "{{ slurp_account_key.content | b64decode }}"
|
||||
challenge: http-01
|
||||
modify_account: yes
|
||||
deactivate_authzs: no
|
||||
@@ -76,8 +80,8 @@
|
||||
- name: Revoke certificate 1 via account key
|
||||
acme_certificate_revoke:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
||||
certificate: "{{ output_dir }}/cert-1.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
|
||||
certificate: "{{ remote_tmp_dir }}/cert-1.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
@@ -86,19 +90,23 @@
|
||||
- name: Revoke certificate 2 via certificate private key
|
||||
acme_certificate_revoke:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
private_key_src: "{{ output_dir }}/cert-2.key"
|
||||
private_key_src: "{{ remote_tmp_dir }}/cert-2.key"
|
||||
private_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||
certificate: "{{ output_dir }}/cert-2.pem"
|
||||
certificate: "{{ remote_tmp_dir }}/cert-2.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
ignore_errors: yes
|
||||
register: cert_2_revoke
|
||||
- name: Read account key (RSA)
|
||||
slurp:
|
||||
src: '{{ remote_tmp_dir }}/account-rsa.pem'
|
||||
register: slurp_account_key
|
||||
- name: Revoke certificate 3 via account key (fullchain)
|
||||
acme_certificate_revoke:
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa.pem') }}"
|
||||
certificate: "{{ output_dir }}/cert-3-fullchain.pem"
|
||||
account_key_content: "{{ slurp_account_key.content | b64decode }}"
|
||||
certificate: "{{ remote_tmp_dir }}/cert-3-fullchain.pem"
|
||||
acme_version: 2
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
validate_certs: no
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
- name: Remove output directory
|
||||
file:
|
||||
path: "{{ output_dir }}"
|
||||
path: "{{ remote_tmp_dir }}"
|
||||
state: absent
|
||||
|
||||
- name: Re-create output directory
|
||||
file:
|
||||
path: "{{ output_dir }}"
|
||||
path: "{{ remote_tmp_dir }}"
|
||||
state: directory
|
||||
|
||||
- block:
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
shippable/cloud/group1
|
||||
cloud/acme
|
||||
|
||||
# Since skipping below fails miserably with ansible-core 2.11 and earlier, we have to skip all POSIX tests...
|
||||
# (https://github.com/ansible/ansible/issues/75711)
|
||||
# shippable/posix/group1
|
||||
|
||||
# Skip all VMs, since we cannot talk to the ACME simulator from these:
|
||||
# (TODO: remove when ansible-core 2.12 is the earliest version we support)
|
||||
# skip/aix
|
||||
# skip/freebsd
|
||||
# skip/macos
|
||||
# skip/osx
|
||||
# skip/rhel
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
dependencies:
|
||||
- setup_acme
|
||||
- setup_remote_tmp_dir
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- block:
|
||||
- name: Generate ECC256 accoun keys
|
||||
openssl_privatekey:
|
||||
path: "{{ output_dir }}/account-ec256.pem"
|
||||
path: "{{ remote_tmp_dir }}/account-ec256.pem"
|
||||
type: ECC
|
||||
curve: secp256r1
|
||||
force: true
|
||||
@@ -31,4 +31,4 @@
|
||||
terms_agreed: yes
|
||||
account_email: "example@example.org"
|
||||
|
||||
when: openssl_version.stdout is version('1.0.0', '>=') or cryptography_version.stdout is version('1.5', '>=')
|
||||
when: cryptography_version.stdout is version('1.5', '>=')
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
shippable/cloud/group1
|
||||
cloud/acme
|
||||
|
||||
# Since skipping below fails miserably with ansible-core 2.11 and earlier, we have to skip all POSIX tests...
|
||||
# (https://github.com/ansible/ansible/issues/75711)
|
||||
# shippable/posix/group1
|
||||
|
||||
# Skip all VMs, since we cannot talk to the ACME simulator from these:
|
||||
# (TODO: remove when ansible-core 2.12 is the earliest version we support)
|
||||
# skip/aix
|
||||
# skip/freebsd
|
||||
# skip/macos
|
||||
# skip/osx
|
||||
# skip/rhel
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
dependencies:
|
||||
- setup_acme
|
||||
- setup_remote_tmp_dir
|
||||
- prepare_jinja2_compat
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- block:
|
||||
- name: Generate account keys
|
||||
openssl_privatekey:
|
||||
path: "{{ output_dir }}/{{ item }}.pem"
|
||||
path: "{{ remote_tmp_dir }}/{{ item }}.pem"
|
||||
type: ECC
|
||||
curve: secp256r1
|
||||
force: true
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
- name: Parse account keys (to ease debugging some test failures)
|
||||
openssl_privatekey_info:
|
||||
path: "{{ output_dir }}/{{ item }}.pem"
|
||||
path: "{{ remote_tmp_dir }}/{{ item }}.pem"
|
||||
return_private_key_data: true
|
||||
loop: "{{ account_keys }}"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
acme_version: 2
|
||||
validate_certs: no
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
url: "{{ directory.directory.newAccount}}"
|
||||
method: post
|
||||
content: '{"termsOfServiceAgreed":true}'
|
||||
@@ -46,7 +46,7 @@
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
acme_version: 2
|
||||
validate_certs: no
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
account_uri: "{{ account_creation.headers.location }}"
|
||||
url: "{{ account_creation.headers.location }}"
|
||||
method: get
|
||||
@@ -58,7 +58,7 @@
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
acme_version: 2
|
||||
validate_certs: no
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
account_uri: "{{ account_creation.headers.location }}"
|
||||
url: "{{ account_creation.headers.location }}"
|
||||
method: post
|
||||
@@ -77,7 +77,7 @@
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
acme_version: 2
|
||||
validate_certs: no
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
account_uri: "{{ account_creation.headers.location }}"
|
||||
url: "{{ directory.directory.newOrder }}"
|
||||
method: post
|
||||
@@ -100,7 +100,7 @@
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
acme_version: 2
|
||||
validate_certs: no
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
account_uri: "{{ account_creation.headers.location }}"
|
||||
url: "{{ new_order.headers.location }}"
|
||||
method: get
|
||||
@@ -112,7 +112,7 @@
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
acme_version: 2
|
||||
validate_certs: no
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
account_uri: "{{ account_creation.headers.location }}"
|
||||
url: "{{ item }}"
|
||||
method: get
|
||||
@@ -125,7 +125,7 @@
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
acme_version: 2
|
||||
validate_certs: no
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
account_uri: "{{ account_creation.headers.location }}"
|
||||
url: "{{ (item.challenges | selectattr('type', 'equalto', 'http-01') | list)[0].url }}"
|
||||
method: get
|
||||
@@ -138,7 +138,7 @@
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
acme_version: 2
|
||||
validate_certs: no
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
account_uri: "{{ account_creation.headers.location }}"
|
||||
url: "{{ item.url }}"
|
||||
method: post
|
||||
@@ -152,7 +152,7 @@
|
||||
acme_directory: https://{{ acme_host }}:14000/dir
|
||||
acme_version: 2
|
||||
validate_certs: no
|
||||
account_key_src: "{{ output_dir }}/accountkey.pem"
|
||||
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
|
||||
account_uri: "{{ account_creation.headers.location }}"
|
||||
url: "{{ item.url }}"
|
||||
method: get
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
- name: Remove output directory
|
||||
file:
|
||||
path: "{{ output_dir }}"
|
||||
path: "{{ remote_tmp_dir }}"
|
||||
state: absent
|
||||
|
||||
- name: Re-create output directory
|
||||
file:
|
||||
path: "{{ output_dir }}"
|
||||
path: "{{ remote_tmp_dir }}"
|
||||
state: directory
|
||||
|
||||
- block:
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
shippable/cloud/group1
|
||||
shippable/posix/group1
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
dependencies:
|
||||
- setup_openssl
|
||||
- setup_remote_tmp_dir
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
# 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
|
||||
- block:
|
||||
|
||||
- name: Make sure testhost directory exists
|
||||
file:
|
||||
path: '{{ remote_tmp_dir }}/files/'
|
||||
@@ -16,69 +14,145 @@
|
||||
copy:
|
||||
src: '{{ role_path }}/files/'
|
||||
dest: '{{ remote_tmp_dir }}/files/'
|
||||
remote_src: yes
|
||||
- name: Find root for cert 1
|
||||
|
||||
- block:
|
||||
- name: Find root for cert 1 using directory
|
||||
certificate_complete_chain:
|
||||
input_chain: '{{ fullchain | trim }}'
|
||||
root_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/roots/'
|
||||
register: cert1_root
|
||||
- name: Verify root for cert 1
|
||||
assert:
|
||||
that:
|
||||
- cert1_root.complete_chain | join('') == (fullchain ~ root)
|
||||
- cert1_root.root == root
|
||||
vars:
|
||||
fullchain: "{{ lookup('file', 'cert1-fullchain.pem', rstrip=False) }}"
|
||||
root: "{{ lookup('file', 'cert1-root.pem', rstrip=False) }}"
|
||||
|
||||
- block:
|
||||
- name: Find rootchain for cert 1 using intermediate and root PEM
|
||||
certificate_complete_chain:
|
||||
input_chain: '{{ cert }}'
|
||||
intermediate_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/cert1-chain.pem'
|
||||
root_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/roots.pem'
|
||||
register: cert1_rootchain
|
||||
- name: Verify rootchain for cert 1
|
||||
assert:
|
||||
that:
|
||||
- cert1_rootchain.complete_chain | join('') == (cert ~ chain ~ root)
|
||||
- cert1_rootchain.chain[:-1] | join('') == chain
|
||||
- cert1_rootchain.root == root
|
||||
vars:
|
||||
cert: "{{ lookup('file', 'cert1.pem', rstrip=False) }}"
|
||||
chain: "{{ lookup('file', 'cert1-chain.pem', rstrip=False) }}"
|
||||
root: "{{ lookup('file', 'cert1-root.pem', rstrip=False) }}"
|
||||
|
||||
- block:
|
||||
- name: Find root for cert 2 using directory
|
||||
certificate_complete_chain:
|
||||
input_chain: "{{ fullchain | trim }}"
|
||||
root_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/roots/'
|
||||
register: cert2_root
|
||||
- name: Verify root for cert 2
|
||||
assert:
|
||||
that:
|
||||
- cert2_root.complete_chain | join('') == (fullchain ~ root)
|
||||
- cert2_root.root == root
|
||||
vars:
|
||||
fullchain: "{{ lookup('file', 'cert2-fullchain.pem', rstrip=False) }}"
|
||||
root: "{{ lookup('file', 'cert2-root.pem', rstrip=False) }}"
|
||||
|
||||
- block:
|
||||
- name: Find rootchain for cert 2 using intermediate and root PEM
|
||||
certificate_complete_chain:
|
||||
input_chain: '{{ cert }}'
|
||||
intermediate_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/cert2-chain.pem'
|
||||
root_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/roots.pem'
|
||||
register: cert2_rootchain
|
||||
- name: Verify rootchain for cert 2
|
||||
assert:
|
||||
that:
|
||||
- cert2_rootchain.complete_chain | join('') == (cert ~ chain ~ root)
|
||||
- cert2_rootchain.chain[:-1] | join('') == chain
|
||||
- cert2_rootchain.root == root
|
||||
vars:
|
||||
cert: "{{ lookup('file', 'cert2.pem', rstrip=False) }}"
|
||||
chain: "{{ lookup('file', 'cert2-chain.pem', rstrip=False) }}"
|
||||
root: "{{ lookup('file', 'cert2-root.pem', rstrip=False) }}"
|
||||
|
||||
- block:
|
||||
- name: Find alternate rootchain for cert 2 using intermediate and root PEM
|
||||
certificate_complete_chain:
|
||||
input_chain: '{{ cert }}'
|
||||
intermediate_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/cert2-altchain.pem'
|
||||
root_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/roots.pem'
|
||||
register: cert2_rootchain_alt
|
||||
- name: Verify rootchain for cert 2
|
||||
assert:
|
||||
that:
|
||||
- cert2_rootchain_alt.complete_chain | join('') == (cert ~ chain ~ root)
|
||||
- cert2_rootchain_alt.chain[:-1] | join('') == chain
|
||||
- cert2_rootchain_alt.root == root
|
||||
vars:
|
||||
cert: "{{ lookup('file', 'cert2.pem', rstrip=False) }}"
|
||||
chain: "{{ lookup('file', 'cert2-altchain.pem', rstrip=False) }}"
|
||||
root: "{{ lookup('file', 'cert2-altroot.pem', rstrip=False) }}"
|
||||
|
||||
- block:
|
||||
- name: Find alternate rootchain for cert 2 when complete chain is already presented to the module
|
||||
certificate_complete_chain:
|
||||
input_chain: '{{ cert ~ chain ~ root }}'
|
||||
root_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/roots.pem'
|
||||
register: cert2_complete_chain
|
||||
- name: Verify rootchain for cert 2
|
||||
assert:
|
||||
that:
|
||||
- cert2_complete_chain.complete_chain | join('') == (cert ~ chain ~ root)
|
||||
- cert2_complete_chain.chain == []
|
||||
- cert2_complete_chain.root == root
|
||||
vars:
|
||||
cert: "{{ lookup('file', 'cert2.pem', rstrip=False) }}"
|
||||
chain: "{{ lookup('file', 'cert2-altchain.pem', rstrip=False) }}"
|
||||
root: "{{ lookup('file', 'cert2-altroot.pem', rstrip=False) }}"
|
||||
|
||||
- name: Check failure when no intermediate certificate can be found
|
||||
certificate_complete_chain:
|
||||
input_chain: '{{ lookup(''file'', ''cert1-fullchain.pem'', rstrip=False) }}'
|
||||
root_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/roots/'
|
||||
register: cert1_root
|
||||
- name: Verify root for cert 1
|
||||
assert:
|
||||
that:
|
||||
- cert1_root.complete_chain | join('') == (lookup('file', 'cert1.pem', rstrip=False) ~ lookup('file', 'cert1-chain.pem', rstrip=False) ~ lookup('file', 'cert1-root.pem', rstrip=False))
|
||||
- cert1_root.root == lookup('file', 'cert1-root.pem', rstrip=False)
|
||||
- name: Find rootchain for cert 1
|
||||
certificate_complete_chain:
|
||||
input_chain: '{{ lookup(''file'', ''cert1.pem'', rstrip=False) }}'
|
||||
input_chain: '{{ lookup("file", "cert2.pem", rstrip=True) }}'
|
||||
intermediate_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/cert1-chain.pem'
|
||||
root_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/roots.pem'
|
||||
register: cert1_rootchain
|
||||
- name: Verify rootchain for cert 1
|
||||
register: cert2_no_intermediate
|
||||
ignore_errors: true
|
||||
- name: Verify failure
|
||||
assert:
|
||||
that:
|
||||
- cert1_rootchain.complete_chain | join('') == (lookup('file', 'cert1.pem', rstrip=False) ~ lookup('file', 'cert1-chain.pem', rstrip=False) ~ lookup('file', 'cert1-root.pem', rstrip=False))
|
||||
- cert1_rootchain.chain[:-1] | join('') == lookup('file', 'cert1-chain.pem', rstrip=False)
|
||||
- cert1_rootchain.root == lookup('file', 'cert1-root.pem', rstrip=False)
|
||||
- name: Find root for cert 2
|
||||
- cert2_no_intermediate is failed
|
||||
- "cert2_no_intermediate.msg.startswith('Cannot complete chain. Stuck at certificate ')"
|
||||
|
||||
- name: Check failure when infinite loop is found
|
||||
certificate_complete_chain:
|
||||
input_chain: '{{ lookup(''file'', ''cert2-fullchain.pem'', rstrip=False) }}'
|
||||
root_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/roots/'
|
||||
register: cert2_root
|
||||
- name: Verify root for cert 2
|
||||
assert:
|
||||
that:
|
||||
- cert2_root.complete_chain | join('') == (lookup('file', 'cert2.pem', rstrip=False) ~ lookup('file', 'cert2-chain.pem', rstrip=False) ~ lookup('file', 'cert2-root.pem', rstrip=False))
|
||||
- cert2_root.root == lookup('file', 'cert2-root.pem', rstrip=False)
|
||||
- name: Find rootchain for cert 2
|
||||
certificate_complete_chain:
|
||||
input_chain: '{{ lookup(''file'', ''cert2.pem'', rstrip=False) }}'
|
||||
input_chain: '{{ lookup("file", "cert2-fullchain.pem", rstrip=True) }}'
|
||||
intermediate_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/cert2-chain.pem'
|
||||
root_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/roots.pem'
|
||||
register: cert2_rootchain
|
||||
- name: Verify rootchain for cert 2
|
||||
root_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/cert1-chain.pem'
|
||||
register: cert2_infinite_loop
|
||||
ignore_errors: true
|
||||
- name: Verify failure
|
||||
assert:
|
||||
that:
|
||||
- cert2_rootchain.complete_chain | join('') == (lookup('file', 'cert2.pem', rstrip=False) ~ lookup('file', 'cert2-chain.pem', rstrip=False) ~ lookup('file', 'cert2-root.pem', rstrip=False))
|
||||
- cert2_rootchain.chain[:-1] | join('') == lookup('file', 'cert2-chain.pem', rstrip=False)
|
||||
- cert2_rootchain.root == lookup('file', 'cert2-root.pem', rstrip=False)
|
||||
- name: Find alternate rootchain for cert 2
|
||||
certificate_complete_chain:
|
||||
input_chain: '{{ lookup(''file'', ''cert2.pem'', rstrip=True) }}'
|
||||
intermediate_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/cert2-altchain.pem'
|
||||
root_certificates:
|
||||
- '{{ remote_tmp_dir }}/files/roots.pem'
|
||||
register: cert2_rootchain_alt
|
||||
- name: Verify rootchain for cert 2
|
||||
assert:
|
||||
that:
|
||||
- cert2_rootchain_alt.complete_chain | join('') == (lookup('file', 'cert2.pem', rstrip=False) ~ lookup('file', 'cert2-altchain.pem', rstrip=False) ~ lookup('file', 'cert2-altroot.pem', rstrip=False))
|
||||
- cert2_rootchain_alt.chain[:-1] | join('') == lookup('file', 'cert2-altchain.pem', rstrip=False)
|
||||
- cert2_rootchain_alt.root == lookup('file', 'cert2-altroot.pem', rstrip=False)
|
||||
- cert2_infinite_loop is failed
|
||||
- "cert2_infinite_loop.msg == 'Found cycle while building certificate chain'"
|
||||
|
||||
when: cryptography_version.stdout is version('1.5', '>=')
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
shippable/cloud/group1
|
||||
shippable/posix/group1
|
||||
destructive
|
||||
needs/httptester
|
||||
|
||||
@@ -4,16 +4,35 @@
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
- set_fact:
|
||||
skip_tests: false
|
||||
|
||||
- block:
|
||||
|
||||
- name: Get servers certificate with backend auto-detection
|
||||
get_certificate:
|
||||
host: "{{ httpbin_host }}"
|
||||
port: 443
|
||||
ignore_errors: true
|
||||
register: result
|
||||
|
||||
- set_fact:
|
||||
skip_tests: |
|
||||
{{
|
||||
result is failed and (
|
||||
'error: [Errno 1] _ssl.c:492: error:14094410:SSL routines:SSL3_READ_BYTES:sslv3 alert handshake failure' in result.msg
|
||||
or
|
||||
'error: _ssl.c:314: Invalid SSL protocol variant specified.' in result.msg
|
||||
)
|
||||
}}
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is success or skip_tests
|
||||
|
||||
when: |
|
||||
pyopenssl_version.stdout is version('0.15', '>=') or
|
||||
(cryptography_version.stdout is version('1.6', '>=') and (ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6))
|
||||
cryptography_version.stdout is version('1.6', '>=')
|
||||
|
||||
- block:
|
||||
|
||||
@@ -21,17 +40,7 @@
|
||||
vars:
|
||||
select_crypto_backend: pyopenssl
|
||||
|
||||
when: pyopenssl_version.stdout is version('0.15', '>=')
|
||||
|
||||
- name: Remove output directory
|
||||
file:
|
||||
path: "{{ output_dir }}"
|
||||
state: absent
|
||||
|
||||
- name: Re-create output directory
|
||||
file:
|
||||
path: "{{ output_dir }}"
|
||||
state: directory
|
||||
when: pyopenssl_version.stdout is version('0.15', '>=') and not skip_tests
|
||||
|
||||
- block:
|
||||
|
||||
@@ -42,6 +51,4 @@
|
||||
# The module doesn't work with CentOS 6. Since the pyOpenSSL installed there is too old,
|
||||
# we never noticed before. This becomes a problem with the new cryptography backend,
|
||||
# since there is a new enough cryptography version...
|
||||
when: |
|
||||
cryptography_version.stdout is version('1.6', '>=') and
|
||||
(ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6)
|
||||
when: cryptography_version.stdout is version('1.6', '>=') and not skip_tests
|
||||
|
||||
@@ -97,14 +97,19 @@
|
||||
# We got the correct response from the module
|
||||
- "'ca_cert file does not exist' == result.msg"
|
||||
|
||||
- name: Get a temp directory
|
||||
tempfile:
|
||||
state: directory
|
||||
register: my_temp_dir
|
||||
|
||||
- name: Download CA Cert as pem from server
|
||||
get_url:
|
||||
url: "http://ansible.http.tests/cacert.pem"
|
||||
dest: "{{ output_dir }}/temp.pem"
|
||||
dest: "{{ my_temp_dir.path }}/temp.pem"
|
||||
|
||||
- name: Get servers certificate comparing it to its own ca_cert file
|
||||
get_certificate:
|
||||
ca_cert: '{{ output_dir }}/temp.pem'
|
||||
ca_cert: '{{ my_temp_dir.path }}/temp.pem'
|
||||
host: "{{ httpbin_host }}"
|
||||
port: 443
|
||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||
@@ -115,11 +120,6 @@
|
||||
- result is not changed
|
||||
- result is not failed
|
||||
|
||||
- name: Get a temp directory
|
||||
tempfile:
|
||||
state: directory
|
||||
register: my_temp_dir
|
||||
|
||||
- name: Deploy the bogus_ca.pem file
|
||||
copy:
|
||||
src: "bogus_ca.pem"
|
||||
|
||||
2
tests/integration/targets/luks_device/meta/main.yml
Normal file
2
tests/integration/targets/luks_device/meta/main.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
dependencies:
|
||||
- setup_remote_tmp_dir
|
||||
@@ -4,18 +4,25 @@
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
- name: Copy keyfiles
|
||||
copy:
|
||||
src: '{{ item }}'
|
||||
dest: '{{ remote_tmp_dir }}/{{ item }}'
|
||||
loop:
|
||||
- keyfile1
|
||||
- keyfile2
|
||||
- name: Make sure cryptsetup is installed
|
||||
package:
|
||||
name: cryptsetup
|
||||
state: present
|
||||
become: yes
|
||||
- name: Create cryptfile
|
||||
command: dd if=/dev/zero of={{ output_dir.replace('~', ansible_env.HOME) }}/cryptfile bs=1M count=32
|
||||
command: dd if=/dev/zero of={{ remote_tmp_dir.replace('~', ansible_env.HOME) }}/cryptfile bs=1M count=32
|
||||
- name: Create lookback device
|
||||
command: losetup -f {{ output_dir.replace('~', ansible_env.HOME) }}/cryptfile
|
||||
command: losetup -f {{ remote_tmp_dir.replace('~', ansible_env.HOME) }}/cryptfile
|
||||
become: yes
|
||||
- name: Determine loop device name
|
||||
command: losetup -j {{ output_dir.replace('~', ansible_env.HOME) }}/cryptfile --output name
|
||||
command: losetup -j {{ remote_tmp_dir.replace('~', ansible_env.HOME) }}/cryptfile --output name
|
||||
become: yes
|
||||
register: cryptfile_device_output
|
||||
- set_fact:
|
||||
@@ -37,5 +44,5 @@
|
||||
- command: losetup -d "{{ cryptfile_device }}"
|
||||
become: yes
|
||||
- file:
|
||||
dest: "{{ output_dir }}/cryptfile"
|
||||
dest: "{{ remote_tmp_dir }}/cryptfile"
|
||||
state: absent
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
check_mode: yes
|
||||
@@ -13,7 +13,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
@@ -22,7 +22,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
@@ -31,7 +31,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
check_mode: yes
|
||||
@@ -48,7 +48,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
check_mode: yes
|
||||
become: yes
|
||||
register: open_check
|
||||
@@ -56,21 +56,21 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
become: yes
|
||||
register: open
|
||||
- name: Open (idempotent)
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
become: yes
|
||||
register: open_idem
|
||||
- name: Open (idempotent, check)
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
check_mode: yes
|
||||
become: yes
|
||||
register: open_idem_check
|
||||
@@ -118,7 +118,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
become: yes
|
||||
|
||||
- name: Closed (via device, check)
|
||||
@@ -158,7 +158,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
become: yes
|
||||
|
||||
- name: Absent (check)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
luks_device:
|
||||
device: /dev/asdfasdfasdf
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
check_mode: yes
|
||||
@@ -14,7 +14,7 @@
|
||||
luks_device:
|
||||
device: /dev/asdfasdfasdf
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
ignore_errors: yes
|
||||
@@ -31,7 +31,7 @@
|
||||
luks_device:
|
||||
device: /tmp/
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
check_mode: yes
|
||||
@@ -42,7 +42,7 @@
|
||||
luks_device:
|
||||
device: /tmp/
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
ignore_errors: yes
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
@@ -14,7 +14,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
register: open_try
|
||||
@@ -31,7 +31,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile2"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile2"
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
register: open_try
|
||||
@@ -43,8 +43,8 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
new_keyfile: "{{ role_path }}/files/keyfile2"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
new_keyfile: "{{ remote_tmp_dir }}/keyfile2"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
@@ -54,8 +54,8 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
new_keyfile: "{{ role_path }}/files/keyfile2"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
new_keyfile: "{{ remote_tmp_dir }}/keyfile2"
|
||||
become: yes
|
||||
register: result_2
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile2"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile2"
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
register: open_try
|
||||
@@ -91,8 +91,8 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
remove_keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
remove_keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
become: yes
|
||||
register: result_1
|
||||
|
||||
@@ -100,8 +100,8 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
remove_keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
remove_keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
become: yes
|
||||
register: result_2
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
register: open_try
|
||||
@@ -128,7 +128,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile2"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile2"
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
register: open_try
|
||||
@@ -149,8 +149,8 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile2"
|
||||
remove_keyfile: "{{ role_path }}/files/keyfile2"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile2"
|
||||
remove_keyfile: "{{ remote_tmp_dir }}/keyfile2"
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
register: remove_last_key
|
||||
@@ -165,7 +165,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile2"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile2"
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
register: open_try
|
||||
@@ -182,8 +182,8 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile2"
|
||||
remove_keyfile: "{{ role_path }}/files/keyfile2"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile2"
|
||||
remove_keyfile: "{{ remote_tmp_dir }}/keyfile2"
|
||||
force_remove_last_key: yes
|
||||
become: yes
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile2"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile2"
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
register: open_try
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
keysize: 256
|
||||
pbkdf:
|
||||
iteration_count: 1000
|
||||
@@ -13,7 +13,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
keysize: 256
|
||||
pbkdf:
|
||||
iteration_count: 1000
|
||||
@@ -23,7 +23,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
keysize: 512
|
||||
pbkdf:
|
||||
iteration_count: 1000
|
||||
@@ -33,7 +33,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: present
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
pbkdf:
|
||||
iteration_count: 1000
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
state: closed
|
||||
passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
new_passphrase: "{{ cryptfile_passphrase2 }}"
|
||||
new_keyfile: "{{ role_path }}/files/keyfile1"
|
||||
new_keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
@@ -122,7 +122,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
register: open_try
|
||||
@@ -135,7 +135,7 @@
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
new_keyfile: "{{ role_path }}/files/keyfile1"
|
||||
new_keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
become: yes
|
||||
@@ -144,7 +144,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
remove_keyfile: "{{ role_path }}/files/keyfile1"
|
||||
remove_keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
remove_passphrase: "{{ cryptfile_passphrase1 }}"
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
@@ -157,7 +157,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: opened
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
become: yes
|
||||
ignore_errors: yes
|
||||
register: open_try
|
||||
@@ -219,7 +219,7 @@
|
||||
luks_device:
|
||||
device: "{{ cryptfile_device }}"
|
||||
state: closed
|
||||
keyfile: "{{ role_path }}/files/keyfile1"
|
||||
keyfile: "{{ remote_tmp_dir }}/keyfile1"
|
||||
new_passphrase: "{{ cryptfile_passphrase3 }}"
|
||||
pbkdf:
|
||||
iteration_time: 0.1
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
dependencies:
|
||||
- setup_ssh_keygen
|
||||
- setup_ssh_agent
|
||||
- setup_remote_tmp_dir
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
- name: Declare global variables
|
||||
set_fact:
|
||||
signing_key: '{{ output_dir }}/id_key'
|
||||
public_key: '{{ output_dir }}/id_key.pub'
|
||||
certificate_path: '{{ output_dir }}/id_cert'
|
||||
signing_key: '{{ remote_tmp_dir }}/id_key'
|
||||
public_key: '{{ remote_tmp_dir }}/id_key.pub'
|
||||
certificate_path: '{{ remote_tmp_dir }}/id_cert'
|
||||
|
||||
- name: Generate keypair
|
||||
openssh_keypair:
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
####################################################################
|
||||
|
||||
- set_fact:
|
||||
new_signing_key: "{{ output_dir }}/new_key"
|
||||
new_public_key: "{{ output_dir }}/new_key.pub"
|
||||
new_signing_key: "{{ remote_tmp_dir }}/new_key"
|
||||
new_public_key: "{{ remote_tmp_dir }}/new_key.pub"
|
||||
|
||||
- name: Generate new test key
|
||||
openssh_keypair:
|
||||
@@ -20,6 +20,81 @@
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
|
||||
- block:
|
||||
- name: Generate cert with updated signature algorithm
|
||||
openssh_cert:
|
||||
type: user
|
||||
path: "{{ certificate_path }}"
|
||||
public_key: "{{ public_key }}"
|
||||
signing_key: "{{ signing_key }}"
|
||||
signature_algorithm: rsa-sha2-256
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
register: updated_signature_algorithm
|
||||
|
||||
- name: Assert signature algorithm update causes change
|
||||
assert:
|
||||
that:
|
||||
- updated_signature_algorithm is changed
|
||||
|
||||
- name: Generate cert with updated signature algorithm (idempotent)
|
||||
openssh_cert:
|
||||
type: user
|
||||
path: "{{ certificate_path }}"
|
||||
public_key: "{{ public_key }}"
|
||||
signing_key: "{{ signing_key }}"
|
||||
signature_algorithm: rsa-sha2-256
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
register: updated_signature_algorithm_idempotent
|
||||
|
||||
- name: Assert signature algorithm update is idempotent
|
||||
assert:
|
||||
that:
|
||||
- updated_signature_algorithm_idempotent is not changed
|
||||
|
||||
- name: Generate cert with original signature algorithm
|
||||
openssh_cert:
|
||||
type: user
|
||||
path: "{{ certificate_path }}"
|
||||
public_key: "{{ public_key }}"
|
||||
signing_key: "{{ signing_key }}"
|
||||
signature_algorithm: ssh-rsa
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
register: second_signature_algorithm
|
||||
|
||||
- name: Assert second signature algorithm update causes change
|
||||
assert:
|
||||
that:
|
||||
- second_signature_algorithm is changed
|
||||
|
||||
- name: Omit signature algorithm
|
||||
openssh_cert:
|
||||
type: user
|
||||
path: "{{ certificate_path }}"
|
||||
public_key: "{{ public_key }}"
|
||||
signing_key: "{{ signing_key }}"
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
register: omitted_signature_algorithm
|
||||
|
||||
- name: Assert omitted_signature_algorithm does not cause change
|
||||
assert:
|
||||
that:
|
||||
- omitted_signature_algorithm is not changed
|
||||
|
||||
- name: Revert to original certificate
|
||||
openssh_cert:
|
||||
type: user
|
||||
path: "{{ certificate_path }}"
|
||||
public_key: "{{ public_key }}"
|
||||
signing_key: "{{ signing_key }}"
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
regenerate: always
|
||||
when: openssh_version is version("7.3", ">=")
|
||||
|
||||
- name: Generate cert with new signing key
|
||||
openssh_cert:
|
||||
type: user
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
type: user
|
||||
signing_key: "{{ signing_key }}"
|
||||
public_key: "{{ public_key }}"
|
||||
path: '{{ output_dir }}/id_cert_with_agent'
|
||||
path: '{{ remote_tmp_dir }}/id_cert_with_agent'
|
||||
use_agent: true
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
@@ -33,7 +33,7 @@
|
||||
type: user
|
||||
signing_key: "{{ signing_key }}"
|
||||
public_key: "{{ public_key }}"
|
||||
path: '{{ output_dir }}/id_cert_with_agent'
|
||||
path: '{{ remote_tmp_dir }}/id_cert_with_agent'
|
||||
use_agent: true
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
@@ -44,7 +44,7 @@
|
||||
type: user
|
||||
signing_key: "{{ signing_key }}"
|
||||
public_key: "{{ public_key }}"
|
||||
path: '{{ output_dir }}/id_cert_with_agent'
|
||||
path: '{{ remote_tmp_dir }}/id_cert_with_agent'
|
||||
use_agent: true
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
@@ -54,7 +54,7 @@
|
||||
type: user
|
||||
signing_key: "{{ signing_key }}"
|
||||
public_key: "{{ public_key }}"
|
||||
path: '{{ output_dir }}/id_cert_with_agent'
|
||||
path: '{{ remote_tmp_dir }}/id_cert_with_agent'
|
||||
use_agent: true
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
@@ -71,7 +71,7 @@
|
||||
type: user
|
||||
signing_key: "{{ signing_key }}"
|
||||
public_key: "{{ public_key }}"
|
||||
path: '{{ output_dir }}/id_cert_with_agent'
|
||||
path: '{{ remote_tmp_dir }}/id_cert_with_agent'
|
||||
use_agent: true
|
||||
valid_from: always
|
||||
valid_to: forever
|
||||
@@ -80,4 +80,4 @@
|
||||
- name: Remove certificate
|
||||
openssh_cert:
|
||||
state: absent
|
||||
path: '{{ output_dir }}/id_cert_with_agent'
|
||||
path: '{{ remote_tmp_dir }}/id_cert_with_agent'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
dependencies:
|
||||
- setup_ssh_keygen
|
||||
- setup_openssl
|
||||
- setup_bcrypt
|
||||
- setup_bcrypt
|
||||
- setup_remote_tmp_dir
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
- name: Backend auto-detection test
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/auto_backend_key'
|
||||
path: '{{ remote_tmp_dir }}/auto_backend_key'
|
||||
state: "{{ item }}"
|
||||
loop: ['present', 'absent']
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
- name: "({{ backend }}) Generate key (check mode)"
|
||||
openssh_keypair:
|
||||
path: "{{ output_dir }}/core"
|
||||
path: "{{ remote_tmp_dir }}/core"
|
||||
size: 2048
|
||||
backend: "{{ backend }}"
|
||||
register: check_core_output
|
||||
@@ -14,14 +14,14 @@
|
||||
|
||||
- name: "({{ backend }}) Generate key"
|
||||
openssh_keypair:
|
||||
path: "{{ output_dir }}/core"
|
||||
path: "{{ remote_tmp_dir }}/core"
|
||||
size: 2048
|
||||
backend: "{{ backend }}"
|
||||
register: core_output
|
||||
|
||||
- name: "({{ backend }}) Generate key (check mode idempotent)"
|
||||
openssh_keypair:
|
||||
path: "{{ output_dir }}/core"
|
||||
path: "{{ remote_tmp_dir }}/core"
|
||||
size: 2048
|
||||
backend: "{{ backend }}"
|
||||
register: idempotency_check_core_output
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
- name: "({{ backend }}) Generate key (idempotent)"
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/core'
|
||||
path: '{{ remote_tmp_dir }}/core'
|
||||
size: 2048
|
||||
backend: "{{ backend }}"
|
||||
register: idempotency_core_output
|
||||
@@ -52,8 +52,8 @@
|
||||
that:
|
||||
- core_output['fingerprint'] is string
|
||||
- core_output['fingerprint'].startswith('SHA256:')
|
||||
# only distro old enough that it still gives md5 with no prefix
|
||||
when: not (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6')
|
||||
# SHA256 was made the default hashing algorithm for fingerprints in OpenSSH 6.8
|
||||
when: not (backend == 'opensshbin' and openssh_version is version('6.8', '<'))
|
||||
|
||||
- name: "({{ backend }}) Assert key returns public_key"
|
||||
assert:
|
||||
@@ -74,7 +74,7 @@
|
||||
- core_output['type'] == 'rsa'
|
||||
|
||||
- name: "({{ backend }}) Retrieve key size from 'ssh-keygen'"
|
||||
shell: "ssh-keygen -lf {{ output_dir }}/core | grep -o -E '^[0-9]+'"
|
||||
shell: "ssh-keygen -lf {{ remote_tmp_dir }}/core | grep -o -E '^[0-9]+'"
|
||||
register: core_size_ssh_keygen
|
||||
|
||||
- name: "({{ backend }}) Assert key size matches 'ssh-keygen' output"
|
||||
@@ -82,13 +82,18 @@
|
||||
that:
|
||||
- core_size_ssh_keygen.stdout == '2048'
|
||||
|
||||
- name: "({{ backend }}) Read core.pub"
|
||||
slurp:
|
||||
src: '{{ remote_tmp_dir }}/core.pub'
|
||||
register: slurp
|
||||
|
||||
- name: "({{ backend }}) Assert public key module return equal to the public key content"
|
||||
assert:
|
||||
that:
|
||||
- "core_output.public_key == lookup('file', output_dir ~ '/core.pub').strip('\n')"
|
||||
- "core_output.public_key == (slurp.content | b64decode).strip('\n ')"
|
||||
|
||||
- name: "({{ backend }}) Remove key"
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/core'
|
||||
path: '{{ remote_tmp_dir }}/core'
|
||||
backend: "{{ backend }}"
|
||||
state: absent
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
- name: Generate a password protected key
|
||||
command: 'ssh-keygen -f {{ output_dir }}/password_protected -N {{ passphrase }}'
|
||||
command: 'ssh-keygen -f {{ remote_tmp_dir }}/password_protected -N {{ passphrase }}'
|
||||
|
||||
- name: Modify the password protected key with passphrase
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/password_protected'
|
||||
path: '{{ remote_tmp_dir }}/password_protected'
|
||||
size: 1024
|
||||
passphrase: "{{ passphrase }}"
|
||||
backend: cryptography
|
||||
@@ -12,14 +12,14 @@
|
||||
|
||||
- name: Check password protected key idempotency
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/password_protected'
|
||||
path: '{{ remote_tmp_dir }}/password_protected'
|
||||
size: 1024
|
||||
passphrase: "{{ passphrase }}"
|
||||
backend: cryptography
|
||||
register: password_protected_idempotency_output
|
||||
|
||||
- name: Ensure that ssh-keygen can read keys generated with passphrase
|
||||
command: 'ssh-keygen -yf {{ output_dir }}/password_protected -P {{ passphrase }}'
|
||||
command: 'ssh-keygen -yf {{ remote_tmp_dir }}/password_protected -P {{ passphrase }}'
|
||||
register: password_protected_ssh_keygen_output
|
||||
|
||||
- name: Check that password protected key with passphrase was regenerated
|
||||
@@ -31,18 +31,18 @@
|
||||
|
||||
- name: Remove password protected key
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/password_protected'
|
||||
path: '{{ remote_tmp_dir }}/password_protected'
|
||||
backend: cryptography
|
||||
state: absent
|
||||
|
||||
- name: Generate an unprotected key
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/unprotected'
|
||||
path: '{{ remote_tmp_dir }}/unprotected'
|
||||
backend: cryptography
|
||||
|
||||
- name: Modify unprotected key with passphrase
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/unprotected'
|
||||
path: '{{ remote_tmp_dir }}/unprotected'
|
||||
size: 2048
|
||||
passphrase: "{{ passphrase }}"
|
||||
backend: cryptography
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
- name: Modify unprotected key with passphrase (force)
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/unprotected'
|
||||
path: '{{ remote_tmp_dir }}/unprotected'
|
||||
size: 2048
|
||||
passphrase: "{{ passphrase }}"
|
||||
force: true
|
||||
@@ -66,16 +66,16 @@
|
||||
|
||||
- name: Remove unprotected key
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/unprotected'
|
||||
path: '{{ remote_tmp_dir }}/unprotected'
|
||||
backend: cryptography
|
||||
state: absent
|
||||
|
||||
- name: Generate PEM encoded key with passphrase
|
||||
command: 'ssh-keygen -b 4096 -f {{ output_dir }}/pem_encoded -N {{ passphrase }} -m PEM'
|
||||
command: 'ssh-keygen -b 4096 -f {{ remote_tmp_dir }}/pem_encoded -N {{ passphrase }} -m PEM'
|
||||
|
||||
- name: Try to verify a PEM encoded key
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/pem_encoded'
|
||||
path: '{{ remote_tmp_dir }}/pem_encoded'
|
||||
passphrase: "{{ passphrase }}"
|
||||
backend: cryptography
|
||||
register: pem_encoded_output
|
||||
@@ -87,6 +87,6 @@
|
||||
|
||||
- name: Remove PEM encoded key
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/pem_encoded'
|
||||
path: '{{ remote_tmp_dir }}/pem_encoded'
|
||||
backend: cryptography
|
||||
state: absent
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
content: ''
|
||||
mode: '0700'
|
||||
loop:
|
||||
- "{{ output_dir }}/broken"
|
||||
- "{{ output_dir }}/broken.pub"
|
||||
- "{{ remote_tmp_dir }}/broken"
|
||||
- "{{ remote_tmp_dir }}/broken.pub"
|
||||
|
||||
- name: "({{ backend }}) Regenerate key - broken"
|
||||
openssh_keypair:
|
||||
path: "{{ output_dir }}/broken"
|
||||
path: "{{ remote_tmp_dir }}/broken"
|
||||
backend: "{{ backend }}"
|
||||
register: broken_output
|
||||
ignore_errors: true
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
- name: "({{ backend }}) Regenerate key with force - broken"
|
||||
openssh_keypair:
|
||||
path: "{{ output_dir }}/broken"
|
||||
path: "{{ remote_tmp_dir }}/broken"
|
||||
backend: "{{ backend }}"
|
||||
force: true
|
||||
register: force_broken_output
|
||||
@@ -40,24 +40,24 @@
|
||||
|
||||
- name: "({{ backend }}) Remove key - broken"
|
||||
openssh_keypair:
|
||||
path: "{{ output_dir }}/broken"
|
||||
path: "{{ remote_tmp_dir }}/broken"
|
||||
backend: "{{ backend }}"
|
||||
state: absent
|
||||
|
||||
- name: "({{ backend }}) Generate key - write-only"
|
||||
openssh_keypair:
|
||||
path: "{{ output_dir }}/write-only"
|
||||
path: "{{ remote_tmp_dir }}/write-only"
|
||||
mode: "0200"
|
||||
backend: "{{ backend }}"
|
||||
|
||||
- name: "({{ backend }}) Check private key status - write-only"
|
||||
stat:
|
||||
path: '{{ output_dir }}/write-only'
|
||||
path: '{{ remote_tmp_dir }}/write-only'
|
||||
register: write_only_private_key
|
||||
|
||||
- name: "({{ backend }}) Check public key status - write-only"
|
||||
stat:
|
||||
path: '{{ output_dir }}/write-only.pub'
|
||||
path: '{{ remote_tmp_dir }}/write-only.pub'
|
||||
register: write_only_public_key
|
||||
|
||||
- name: "({{ backend }}) Assert that private and public keys match permissions - write-only"
|
||||
@@ -68,14 +68,14 @@
|
||||
|
||||
- name: "({{ backend }}) Regenerate key with force - write-only"
|
||||
openssh_keypair:
|
||||
path: "{{ output_dir }}/write-only"
|
||||
path: "{{ remote_tmp_dir }}/write-only"
|
||||
backend: "{{ backend }}"
|
||||
force: true
|
||||
register: write_only_output
|
||||
|
||||
- name: "({{ backend }}) Check private key status after regeneration - write-only"
|
||||
stat:
|
||||
path: '{{ output_dir }}/write-only'
|
||||
path: '{{ remote_tmp_dir }}/write-only'
|
||||
register: write_only_private_key_after
|
||||
|
||||
- name: "({{ backend }}) Assert key is regenerated - write-only"
|
||||
@@ -87,26 +87,19 @@
|
||||
assert:
|
||||
that:
|
||||
- write_only_private_key_after.stat.mode == '0200'
|
||||
when: backend == 'opensshbin'
|
||||
|
||||
- name: "({{ backend }}) Assert key permissions are not preserved with 'cryptography'"
|
||||
assert:
|
||||
that:
|
||||
- write_only_private_key_after.stat.mode == '0600'
|
||||
when: backend == 'cryptography'
|
||||
|
||||
- name: "({{ backend }}) Remove key - write-only"
|
||||
openssh_keypair:
|
||||
path: "{{ output_dir }}/write-only"
|
||||
path: "{{ remote_tmp_dir }}/write-only"
|
||||
backend: "{{ backend }}"
|
||||
state: absent
|
||||
|
||||
- name: "({{ backend }}) Generate key with ssh-keygen - password_protected"
|
||||
command: "ssh-keygen -f {{ output_dir }}/password_protected -N {{ passphrase }}"
|
||||
command: "ssh-keygen -f {{ remote_tmp_dir }}/password_protected -N {{ passphrase }}"
|
||||
|
||||
- name: "({{ backend }}) Modify key - password_protected"
|
||||
openssh_keypair:
|
||||
path: "{{ output_dir }}/password_protected"
|
||||
path: "{{ remote_tmp_dir }}/password_protected"
|
||||
size: 2048
|
||||
backend: "{{ backend }}"
|
||||
register: password_protected_output
|
||||
@@ -120,7 +113,7 @@
|
||||
|
||||
- name: "({{ backend }}) Modify key with 'force=true' - password_protected"
|
||||
openssh_keypair:
|
||||
path: "{{ output_dir }}/password_protected"
|
||||
path: "{{ remote_tmp_dir }}/password_protected"
|
||||
size: 2048
|
||||
backend: "{{ backend }}"
|
||||
force: true
|
||||
@@ -133,6 +126,6 @@
|
||||
|
||||
- name: "({{ backend }}) Remove key - password_protected"
|
||||
openssh_keypair:
|
||||
path: "{{ output_dir }}/password_protected"
|
||||
path: "{{ remote_tmp_dir }}/password_protected"
|
||||
backend: "{{ backend }}"
|
||||
state: absent
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user