mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37c7100c8b | ||
|
|
35266bda0e | ||
|
|
6a90a43995 | ||
|
|
096262b6f1 | ||
|
|
03df636e5e | ||
|
|
02bc9de560 | ||
|
|
8fe0a2450e | ||
|
|
d8bb99d547 | ||
|
|
79f9ce437a | ||
|
|
6cb02818ef | ||
|
|
e94fb2dff2 | ||
|
|
14c39b1f99 | ||
|
|
6dafa5954e | ||
|
|
da1dd21a9e | ||
|
|
011036b87f | ||
|
|
35ef2edb3f | ||
|
|
ebcf866891 | ||
|
|
60c6d87b05 | ||
|
|
2aa38fe247 | ||
|
|
d19faa1627 | ||
|
|
e910f299b9 | ||
|
|
2ebf26854e | ||
|
|
7ff067937a | ||
|
|
2727b74cc7 | ||
|
|
3bb9c5f9a7 | ||
|
|
5623590c77 | ||
|
|
29050913b3 | ||
|
|
aead2bf783 | ||
|
|
2aaae70372 | ||
|
|
de29a258c6 | ||
|
|
8ca5b480e4 | ||
|
|
f6ec785b0d | ||
|
|
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 | ||
|
|
408b538a45 | ||
|
|
1e82465559 | ||
|
|
85ac60e2c3 | ||
|
|
aaba87ac57 | ||
|
|
d6403ace6e | ||
|
|
6c989de994 | ||
|
|
4908f1a8ec | ||
|
|
f3c6c1172e | ||
|
|
9658a34605 | ||
|
|
5d153e05ef | ||
|
|
2ba77e015c | ||
|
|
9d958033a5 | ||
|
|
4adad5d98a | ||
|
|
b9737101cd | ||
|
|
9f27e28a45 | ||
|
|
9a7b2f1d0d | ||
|
|
0df33de73e | ||
|
|
cda2edf92c | ||
|
|
d38f59c18c |
@@ -19,6 +19,11 @@ schedules:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- cron: 0 12 * * 0
|
||||
displayName: Weekly (old stable branches)
|
||||
always: true
|
||||
branches:
|
||||
include:
|
||||
- stable-*
|
||||
|
||||
variables:
|
||||
@@ -42,19 +47,30 @@ pool: Standard
|
||||
|
||||
stages:
|
||||
### Sanity & units
|
||||
- stage: Ansible_devel
|
||||
displayName: Sanity & Units devel
|
||||
- stage: Ansible_2_13
|
||||
displayName: Sanity & Units 2.13
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
targets:
|
||||
- name: Sanity
|
||||
test: 'devel/sanity/1'
|
||||
test: '2.13/sanity/1'
|
||||
- name: Sanity Extra # Only on devel
|
||||
test: 'devel/sanity/extra'
|
||||
test: '2.13/sanity/extra'
|
||||
- name: Units
|
||||
test: 'devel/units/1'
|
||||
test: '2.13/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: []
|
||||
@@ -89,24 +105,20 @@ stages:
|
||||
- name: Units
|
||||
test: '2.9/units/1'
|
||||
### Docker
|
||||
- stage: Docker_devel
|
||||
displayName: Docker devel
|
||||
- stage: Docker_2_13
|
||||
displayName: Docker 2.13
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: devel/linux/{0}/1
|
||||
testFormat: 2.13/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,22 @@ 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: 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: []
|
||||
@@ -125,18 +153,12 @@ stages:
|
||||
targets:
|
||||
- name: CentOS 7
|
||||
test: centos7
|
||||
- name: CentOS 8
|
||||
test: centos8
|
||||
- name: Fedora 32
|
||||
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 +169,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,38 +185,44 @@ 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
|
||||
test: ubuntu1804
|
||||
|
||||
### Remote
|
||||
- stage: Remote_devel
|
||||
displayName: Remote devel
|
||||
- stage: Remote_2_13
|
||||
displayName: Remote 2.13
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: devel/{0}/1
|
||||
testFormat: 2.13/{0}/1
|
||||
targets:
|
||||
- name: macOS 12.0
|
||||
test: macos/12.0
|
||||
- name: RHEL 7.9
|
||||
test: rhel/7.9
|
||||
- name: RHEL 8.5
|
||||
test: rhel/8.5
|
||||
- name: FreeBSD 12.3
|
||||
test: freebsd/12.3
|
||||
- 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 7.9
|
||||
test: rhel/7.9
|
||||
- name: RHEL 8.3
|
||||
test: rhel/8.3
|
||||
- name: FreeBSD 12.2
|
||||
test: freebsd/12.2
|
||||
- name: RHEL 8.4
|
||||
test: rhel/8.4
|
||||
- name: FreeBSD 13.0
|
||||
test: freebsd/13.0
|
||||
- stage: Remote_2_11
|
||||
@@ -219,8 +235,8 @@ stages:
|
||||
targets:
|
||||
- name: RHEL 7.9
|
||||
test: rhel/7.9
|
||||
- name: macOS 11.1
|
||||
test: macos/11.1
|
||||
- name: RHEL 8.3
|
||||
test: rhel/8.3
|
||||
- name: FreeBSD 12.2
|
||||
test: freebsd/12.2
|
||||
- stage: Remote_2_10
|
||||
@@ -231,8 +247,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
|
||||
@@ -250,23 +264,33 @@ stages:
|
||||
- name: 'RHEL 7.8'
|
||||
test: 'rhel/7.8'
|
||||
### cloud
|
||||
- stage: Cloud_devel
|
||||
displayName: Cloud devel
|
||||
- stage: Cloud_2_13
|
||||
displayName: Cloud 2.13
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
nameFormat: Python {0}
|
||||
testFormat: devel/cloud/{0}/1
|
||||
testFormat: 2.13/cloud/{0}/1
|
||||
targets:
|
||||
- test: 2.6
|
||||
- test: 2.7
|
||||
- test: 3.5
|
||||
- test: 3.6
|
||||
- test: 3.7
|
||||
- test: 3.8
|
||||
# - 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: []
|
||||
@@ -296,28 +320,32 @@ stages:
|
||||
nameFormat: Python {0}
|
||||
testFormat: 2.9/cloud/{0}/1
|
||||
targets:
|
||||
- test: 3.5
|
||||
- test: 2.7
|
||||
|
||||
## Finally
|
||||
|
||||
- stage: Summary
|
||||
condition: succeededOrFailed()
|
||||
dependsOn:
|
||||
- Ansible_devel
|
||||
- Ansible_2_13
|
||||
- Ansible_2_12
|
||||
- Ansible_2_11
|
||||
- Ansible_2_10
|
||||
- Ansible_2_9
|
||||
- Remote_devel
|
||||
- Docker_devel
|
||||
- Cloud_devel
|
||||
- Remote_2_13
|
||||
- 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_2_13
|
||||
- Docker_2_12
|
||||
- Docker_2_11
|
||||
- Docker_2_10
|
||||
- Docker_2_9
|
||||
- Cloud_2_13
|
||||
- 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
|
||||
|
||||
248
CHANGELOG.rst
248
CHANGELOG.rst
@@ -5,6 +5,254 @@ Community Crypto Release Notes
|
||||
.. contents:: Topics
|
||||
|
||||
|
||||
v1.9.14
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Make collection more robust when PyOpenSSL is used with an incompatible cryptography version (https://github.com/ansible-collections/community.crypto/pull/446).
|
||||
- openssh_* modules - fix exception handling to report traceback to users for enhanced traceability (https://github.com/ansible-collections/community.crypto/pull/417).
|
||||
- x509_crl - fix crash when ``issuer`` for a revoked certificate is specified (https://github.com/ansible-collections/community.crypto/pull/441).
|
||||
|
||||
v1.9.13
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- luks_device - fix parsing of ``lsblk`` output when device name ends with ``crypt`` (https://github.com/ansible-collections/community.crypto/issues/409, https://github.com/ansible-collections/community.crypto/pull/410).
|
||||
|
||||
v1.9.12
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- certificate_complete_chain - allow multiple potential intermediate certificates to have the same subject (https://github.com/ansible-collections/community.crypto/issues/399, https://github.com/ansible-collections/community.crypto/pull/403).
|
||||
- x509_certificate - for the ``ownca`` provider, check whether the CA private key actually belongs to the CA certificate. This fix only covers the ``cryptography`` backend, not the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - regenerate certificate when the CA's public key changes for ``provider=ownca``. This fix only covers the ``cryptography`` backend, not the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - regenerate certificate when the CA's subject changes for ``provider=ownca`` (https://github.com/ansible-collections/community.crypto/issues/400, https://github.com/ansible-collections/community.crypto/pull/402).
|
||||
- x509_certificate - regenerate certificate when the private key changes for ``provider=selfsigned``. This fix only covers the ``cryptography`` backend, not the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
|
||||
- x509_certificate - when using the ``ownca`` provider with the ``pyopenssl`` backend, changing the CA's public key does not cause regeneration of the certificate (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - when using the ``ownca`` provider with the ``pyopenssl`` backend, it is possible to specify a CA private key which is not related to the CA certificate (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - when using the ``selfsigned`` provider with the ``pyopenssl`` backend, changing the private key does not cause regeneration of the certificate (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
|
||||
v1.9.11
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssh_cert - fixed false ``changed`` status for ``host`` certificates when using ``full_idempotence`` (https://github.com/ansible-collections/community.crypto/issues/395, https://github.com/ansible-collections/community.crypto/pull/396).
|
||||
|
||||
v1.9.10
|
||||
=======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- luks_devices - set ``LANG`` and similar environment variables to avoid translated output, which can break some of the module's functionality like key management (https://github.com/ansible-collections/community.crypto/pull/388, https://github.com/ansible-collections/community.crypto/issues/385).
|
||||
|
||||
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
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix and feature release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Avoid internal ansible-core module_utils in favor of equivalent public API available since at least Ansible 2.9 (https://github.com/ansible-collections/community.crypto/pull/253).
|
||||
- openssh certificate module utils - new module_utils for parsing OpenSSH certificates (https://github.com/ansible-collections/community.crypto/pull/246).
|
||||
- openssh_cert - added ``regenerate`` option to validate additional certificate parameters which trigger regeneration of an existing certificate (https://github.com/ansible-collections/community.crypto/pull/256).
|
||||
- openssh_cert - adding ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssh_cert - fixed certificate generation to restore original certificate if an error is encountered (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||
- openssh_keypair - fixed a bug that prevented custom file attributes being applied to public keys (https://github.com/ansible-collections/community.crypto/pull/257).
|
||||
|
||||
v1.7.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssl_pkcs12 - fix crash when loading passphrase-protected PKCS#12 files with ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/issues/247, https://github.com/ansible-collections/community.crypto/pull/248).
|
||||
|
||||
v1.7.0
|
||||
======
|
||||
|
||||
|
||||
@@ -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, ansible-core 2.12 and ansible-core 2.13 releases. Ansible versions before 2.9.10 are not supported.
|
||||
|
||||
## External requirements
|
||||
|
||||
|
||||
@@ -468,3 +468,259 @@ releases:
|
||||
name: openssl_publickey_info
|
||||
namespace: ''
|
||||
release_date: '2021-06-02'
|
||||
1.7.1:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssl_pkcs12 - fix crash when loading passphrase-protected PKCS#12 files
|
||||
with ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/issues/247,
|
||||
https://github.com/ansible-collections/community.crypto/pull/248).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.7.1.yml
|
||||
- 248-openssl_pkcs12-passphrase-fix.yml
|
||||
release_date: '2021-06-11'
|
||||
1.8.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssh_cert - fixed certificate generation to restore original certificate
|
||||
if an error is encountered (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||
- openssh_keypair - fixed a bug that prevented custom file attributes being
|
||||
applied to public keys (https://github.com/ansible-collections/community.crypto/pull/257).
|
||||
minor_changes:
|
||||
- Avoid internal ansible-core module_utils in favor of equivalent public API
|
||||
available since at least Ansible 2.9 (https://github.com/ansible-collections/community.crypto/pull/253).
|
||||
- openssh certificate module utils - new module_utils for parsing OpenSSH certificates
|
||||
(https://github.com/ansible-collections/community.crypto/pull/246).
|
||||
- openssh_cert - added ``regenerate`` option to validate additional certificate
|
||||
parameters which trigger regeneration of an existing certificate (https://github.com/ansible-collections/community.crypto/pull/256).
|
||||
- openssh_cert - adding ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||
release_summary: Regular bugfix and feature release.
|
||||
fragments:
|
||||
- 1.8.0.yml
|
||||
- 246-openssh-certificate-module-utils.yml
|
||||
- 255-openssh_cert-adding-diff-support.yml
|
||||
- 256-openssh_cert-adding-idempotency-option.yml
|
||||
- 257-openssh-keypair-fix-pubkey-permissions.yml
|
||||
- ansible-core-_text.yml
|
||||
release_date: '2021-08-10'
|
||||
1.9.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- keypair_backend module utils - simplify code to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
- openssh_keypair - fixed ``cryptography`` backend to preserve original file
|
||||
permissions when regenerating a keypair requires existing files to be overwritten
|
||||
(https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- openssh_keypair - fixed error handling to restore original keypair if regeneration
|
||||
fails (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- x509_crl - restore inherited function signature to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
minor_changes:
|
||||
- get_certificate - added ``starttls`` option to retrieve certificates from
|
||||
servers which require clients to request an encrypted connection (https://github.com/ansible-collections/community.crypto/pull/264).
|
||||
- openssh_keypair - added ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
release_summary: Regular feature release.
|
||||
fragments:
|
||||
- 1.9.0.yml
|
||||
- 260-openssh_keypair-diff-support.yml
|
||||
- 263-sanity.yml
|
||||
- 264-get_certificate-add-starttls-option.yml
|
||||
release_date: '2021-08-30'
|
||||
1.9.1:
|
||||
changes:
|
||||
release_summary: Accidental 1.9.1 release. Identical to 1.9.0.
|
||||
release_date: '2021-08-30'
|
||||
1.9.10:
|
||||
changes:
|
||||
bugfixes:
|
||||
- luks_devices - set ``LANG`` and similar environment variables to avoid translated
|
||||
output, which can break some of the module's functionality like key management
|
||||
(https://github.com/ansible-collections/community.crypto/pull/388, https://github.com/ansible-collections/community.crypto/issues/385).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.10.yml
|
||||
- 388-luks_device-i18n.yml
|
||||
release_date: '2022-02-01'
|
||||
1.9.11:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssh_cert - fixed false ``changed`` status for ``host`` certificates when
|
||||
using ``full_idempotence`` (https://github.com/ansible-collections/community.crypto/issues/395,
|
||||
https://github.com/ansible-collections/community.crypto/pull/396).
|
||||
release_summary: Bugfix release.
|
||||
fragments:
|
||||
- 1.9.11.yml
|
||||
- 396-openssh_cert-host-cert-idempotence-fix.yml
|
||||
release_date: '2022-02-05'
|
||||
1.9.12:
|
||||
changes:
|
||||
bugfixes:
|
||||
- certificate_complete_chain - allow multiple potential intermediate certificates
|
||||
to have the same subject (https://github.com/ansible-collections/community.crypto/issues/399,
|
||||
https://github.com/ansible-collections/community.crypto/pull/403).
|
||||
- x509_certificate - for the ``ownca`` provider, check whether the CA private
|
||||
key actually belongs to the CA certificate. This fix only covers the ``cryptography``
|
||||
backend, not the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - regenerate certificate when the CA's public key changes
|
||||
for ``provider=ownca``. This fix only covers the ``cryptography`` backend,
|
||||
not the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - regenerate certificate when the CA's subject changes for
|
||||
``provider=ownca`` (https://github.com/ansible-collections/community.crypto/issues/400,
|
||||
https://github.com/ansible-collections/community.crypto/pull/402).
|
||||
- x509_certificate - regenerate certificate when the private key changes for
|
||||
``provider=selfsigned``. This fix only covers the ``cryptography`` backend,
|
||||
not the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
known_issues:
|
||||
- x509_certificate - when using the ``ownca`` provider with the ``pyopenssl``
|
||||
backend, changing the CA's public key does not cause regeneration of the certificate
|
||||
(https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - when using the ``ownca`` provider with the ``pyopenssl``
|
||||
backend, it is possible to specify a CA private key which is not related to
|
||||
the CA certificate (https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
- x509_certificate - when using the ``selfsigned`` provider with the ``pyopenssl``
|
||||
backend, changing the private key does not cause regeneration of the certificate
|
||||
(https://github.com/ansible-collections/community.crypto/pull/407).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.12.yml
|
||||
- 402-x509_certificate-ownca-subject.yml
|
||||
- 403-certificate_complete_chain-same-subject.yml
|
||||
- 407-x509_certificate-signature.yml
|
||||
release_date: '2022-02-21'
|
||||
1.9.13:
|
||||
changes:
|
||||
bugfixes:
|
||||
- luks_device - fix parsing of ``lsblk`` output when device name ends with ``crypt``
|
||||
(https://github.com/ansible-collections/community.crypto/issues/409, https://github.com/ansible-collections/community.crypto/pull/410).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.13.yml
|
||||
- 410-luks_device-lsblk-parsing.yml
|
||||
release_date: '2022-03-04'
|
||||
1.9.14:
|
||||
changes:
|
||||
bugfixes:
|
||||
- Make collection more robust when PyOpenSSL is used with an incompatible cryptography
|
||||
version (https://github.com/ansible-collections/community.crypto/pull/446).
|
||||
- openssh_* modules - fix exception handling to report traceback to users for
|
||||
enhanced traceability (https://github.com/ansible-collections/community.crypto/pull/417).
|
||||
- x509_crl - fix crash when ``issuer`` for a revoked certificate is specified
|
||||
(https://github.com/ansible-collections/community.crypto/pull/441).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.14.yml
|
||||
- 417-openssh_modules-fix-exception-reporting.yml
|
||||
- 441-x509-crl-cert-issuer.yml
|
||||
- 446-fix.yml
|
||||
release_date: '2022-05-09'
|
||||
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.7.0
|
||||
version: 1.9.14
|
||||
readme: README.md
|
||||
authors:
|
||||
- Ansible (github.com/ansible)
|
||||
|
||||
@@ -9,7 +9,7 @@ __metaclass__ = type
|
||||
|
||||
import base64
|
||||
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.plugin_utils.action_module import ActionModuleBase
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -25,7 +25,7 @@ import traceback
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.six.moves.urllib.parse import unquote
|
||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||
get_default_argspec,
|
||||
|
||||
@@ -14,8 +14,9 @@ import json
|
||||
import locale
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils._text import to_bytes
|
||||
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._text 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:
|
||||
|
||||
@@ -16,7 +16,7 @@ import re
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
|
||||
CryptoBackend,
|
||||
@@ -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:
|
||||
|
||||
@@ -14,7 +14,7 @@ import json
|
||||
import re
|
||||
import time
|
||||
|
||||
from ansible.module_utils._text import to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
||||
|
||||
|
||||
@@ -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._text import to_text
|
||||
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
|
||||
|
||||
@@ -14,7 +14,7 @@ import shutil
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import re
|
||||
import textwrap
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible.module_utils.six.moves.urllib.parse import unquote
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
|
||||
|
||||
@@ -8,7 +8,7 @@ __metaclass__ = type
|
||||
|
||||
import re
|
||||
|
||||
from ansible.module_utils._text import to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
|
||||
"""
|
||||
|
||||
@@ -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,13 +20,13 @@ 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
|
||||
from OpenSSL import crypto # noqa
|
||||
HAS_PYOPENSSL = True
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
# Error handled in the calling module.
|
||||
HAS_PYOPENSSL = False
|
||||
|
||||
|
||||
@@ -23,18 +23,44 @@ import base64
|
||||
import binascii
|
||||
import re
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
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.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
pass
|
||||
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.ec
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.dsa
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.ed25519
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.ed448
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
# This is a separate try/except since this is only present in cryptography 2.5 or newer
|
||||
from cryptography.hazmat.primitives.serialization.pkcs12 import (
|
||||
@@ -44,9 +70,23 @@ 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_DSA_SIGN,
|
||||
CRYPTOGRAPHY_HAS_EC_SIGN,
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED25519_SIGN,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
CRYPTOGRAPHY_HAS_ED448_SIGN,
|
||||
CRYPTOGRAPHY_HAS_RSA_SIGN,
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
@@ -64,60 +104,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 +374,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 +388,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,16 +536,111 @@ 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')
|
||||
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
|
||||
|
||||
|
||||
def cryptography_verify_signature(signature, data, hash_algorithm, signer_public_key):
|
||||
'''
|
||||
Check whether the given signature of the given data was signed by the given public key object.
|
||||
'''
|
||||
try:
|
||||
if CRYPTOGRAPHY_HAS_RSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
|
||||
signer_public_key.verify(signature, data, padding.PKCS1v15(), hash_algorithm)
|
||||
return True
|
||||
if CRYPTOGRAPHY_HAS_EC_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
|
||||
signer_public_key.verify(signature, data, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hash_algorithm))
|
||||
return True
|
||||
if CRYPTOGRAPHY_HAS_DSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey):
|
||||
signer_public_key.verify(signature, data, hash_algorithm)
|
||||
return True
|
||||
if CRYPTOGRAPHY_HAS_ED25519_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey):
|
||||
signer_public_key.verify(signature, data)
|
||||
return True
|
||||
if CRYPTOGRAPHY_HAS_ED448_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey):
|
||||
signer_public_key.verify(signature, data)
|
||||
return True
|
||||
raise OpenSSLObjectError(u'Unsupported public key type {0}'.format(type(signer_public_key)))
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
|
||||
def cryptography_verify_certificate_signature(certificate, signer_public_key):
|
||||
'''
|
||||
Check whether the given X509 certificate object was signed by the given public key object.
|
||||
'''
|
||||
return cryptography_verify_signature(
|
||||
certificate.signature,
|
||||
certificate.tbs_certificate_bytes,
|
||||
certificate.signature_hash_algorithm,
|
||||
signer_public_key
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
@@ -45,7 +45,7 @@ try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
|
||||
@@ -12,7 +12,7 @@ import os
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
CertificateError,
|
||||
|
||||
@@ -11,7 +11,7 @@ __metaclass__ = type
|
||||
import abc
|
||||
import datetime
|
||||
|
||||
from ansible.module_utils._text import to_native, to_bytes, to_text
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
parse_name_field,
|
||||
@@ -37,7 +37,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
|
||||
@@ -12,7 +12,7 @@ import datetime
|
||||
import time
|
||||
import os
|
||||
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.ecs.api import ECSClient, RestOperationException, SessionConfigurationException
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@ 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._text import to_native, to_text, to_bytes
|
||||
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,
|
||||
@@ -59,7 +59,7 @@ try:
|
||||
# OpenSSL 1.0.x or older
|
||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
|
||||
@@ -10,10 +10,11 @@ __metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from random import randrange
|
||||
|
||||
from ansible.module_utils._text import to_bytes
|
||||
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,
|
||||
@@ -27,8 +28,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_compare_public_keys,
|
||||
cryptography_key_needs_digest_for_signing,
|
||||
cryptography_serial_number_of_cert,
|
||||
cryptography_verify_certificate_signature,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
@@ -40,7 +43,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
|
||||
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
@@ -106,6 +109,9 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
module.fail_json(msg=str(exc))
|
||||
|
||||
if not cryptography_compare_public_keys(self.ca_cert.public_key(), self.ca_private_key.public_key()):
|
||||
raise CertificateError('The CA private key does not belong to the CA certificate')
|
||||
|
||||
if cryptography_key_needs_digest_for_signing(self.ca_private_key):
|
||||
if self.digest is None:
|
||||
raise CertificateError(
|
||||
@@ -172,6 +178,16 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
|
||||
if super(OwnCACertificateBackendCryptography, self).needs_regeneration():
|
||||
return True
|
||||
|
||||
self._ensure_existing_certificate_loaded()
|
||||
|
||||
# Check whether certificate is signed by CA certificate
|
||||
if not cryptography_verify_certificate_signature(self.existing_certificate, self.ca_cert.public_key()):
|
||||
return True
|
||||
|
||||
# Check subject
|
||||
if self.ca_cert.subject != self.existing_certificate.issuer:
|
||||
return True
|
||||
|
||||
# Check AuthorityKeyIdentifier
|
||||
if self.create_authority_key_identifier:
|
||||
try:
|
||||
@@ -184,7 +200,6 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
expected_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key())
|
||||
|
||||
self._ensure_existing_certificate_loaded()
|
||||
try:
|
||||
ext = self.existing_certificate.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
||||
if ext.value != expected_ext:
|
||||
@@ -296,6 +311,18 @@ class OwnCACertificateBackendPyOpenSSL(CertificateBackend):
|
||||
"""Return bytes for self.cert."""
|
||||
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
|
||||
|
||||
def needs_regeneration(self):
|
||||
if super(OwnCACertificateBackendPyOpenSSL, self).needs_regeneration():
|
||||
return True
|
||||
|
||||
self._ensure_existing_certificate_loaded()
|
||||
|
||||
# Check subject
|
||||
if self.ca_cert.get_subject() != self.existing_certificate.get_issuer():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(OwnCACertificateBackendPyOpenSSL, self).dump(include_certificate)
|
||||
result.update({
|
||||
|
||||
@@ -12,7 +12,7 @@ import os
|
||||
|
||||
from random import randrange
|
||||
|
||||
from ansible.module_utils._text import to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
get_relative_time_option,
|
||||
@@ -22,6 +22,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_key_needs_digest_for_signing,
|
||||
cryptography_serial_number_of_cert,
|
||||
cryptography_verify_certificate_signature,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
@@ -32,7 +33,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
|
||||
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
@@ -134,6 +135,18 @@ class SelfSignedCertificateBackendCryptography(CertificateBackend):
|
||||
"""Return bytes for self.cert."""
|
||||
return self.cert.public_bytes(Encoding.PEM)
|
||||
|
||||
def needs_regeneration(self):
|
||||
if super(SelfSignedCertificateBackendCryptography, self).needs_regeneration():
|
||||
return True
|
||||
|
||||
self._ensure_existing_certificate_loaded()
|
||||
|
||||
# Check whether certificate is signed by private key
|
||||
if not cryptography_verify_certificate_signature(self.existing_certificate, self.privatekey.public_key()):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(SelfSignedCertificateBackendCryptography, self).dump(include_certificate)
|
||||
|
||||
|
||||
@@ -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,11 +12,11 @@ import abc
|
||||
import binascii
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible.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,
|
||||
@@ -63,7 +63,7 @@ try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
@@ -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,11 +13,11 @@ import abc
|
||||
import binascii
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
||||
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,
|
||||
@@ -57,7 +57,7 @@ try:
|
||||
# OpenSSL 1.0.x or older
|
||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
|
||||
@@ -12,11 +12,11 @@ import abc
|
||||
import base64
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils._text import to_bytes
|
||||
from ansible.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,
|
||||
@@ -54,7 +54,7 @@ try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
|
||||
@@ -12,11 +12,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.module_utils._text import to_native, to_bytes
|
||||
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,
|
||||
@@ -49,7 +49,7 @@ try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
|
||||
@@ -10,11 +10,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.module_utils._text import to_native
|
||||
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,
|
||||
@@ -38,7 +38,7 @@ try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
|
||||
@@ -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,13 +21,15 @@ __metaclass__ = type
|
||||
|
||||
import base64
|
||||
|
||||
from ansible.module_utils._text 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:
|
||||
except (ImportError, AttributeError):
|
||||
# Error handled in the calling module.
|
||||
pass
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@ import os
|
||||
import re
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
HAS_PYOPENSSL = True
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
# Error handled in the calling module.
|
||||
HAS_PYOPENSSL = False
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils._text import to_text, to_native
|
||||
from ansible.module_utils.common.text.converters import to_text, to_native
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||
|
||||
337
plugins/module_utils/openssh/backends/common.py
Normal file
337
plugins/module_utils/openssh/backends/common.py
Normal file
@@ -0,0 +1,337 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import abc
|
||||
import os
|
||||
import stat
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils import six
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
|
||||
parse_openssh_version,
|
||||
)
|
||||
|
||||
|
||||
def restore_on_failure(f):
|
||||
def backup_and_restore(module, path, *args, **kwargs):
|
||||
backup_file = module.backup_local(path) if os.path.exists(path) else None
|
||||
|
||||
try:
|
||||
f(module, path, *args, **kwargs)
|
||||
except Exception:
|
||||
if backup_file is not None:
|
||||
module.atomic_move(backup_file, path)
|
||||
raise
|
||||
else:
|
||||
module.add_cleanup_file(backup_file)
|
||||
|
||||
return backup_and_restore
|
||||
|
||||
|
||||
@restore_on_failure
|
||||
def safe_atomic_move(module, path, destination):
|
||||
module.atomic_move(path, destination)
|
||||
|
||||
|
||||
def _restore_all_on_failure(f):
|
||||
def backup_and_restore(self, sources_and_destinations, *args, **kwargs):
|
||||
backups = [(d, self.module.backup_local(d)) for s, d in sources_and_destinations if os.path.exists(d)]
|
||||
|
||||
try:
|
||||
f(self, sources_and_destinations, *args, **kwargs)
|
||||
except Exception:
|
||||
for destination, backup in backups:
|
||||
self.module.atomic_move(backup, destination)
|
||||
raise
|
||||
else:
|
||||
for destination, backup in backups:
|
||||
self.module.add_cleanup_file(backup)
|
||||
return backup_and_restore
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class OpensshModule(object):
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
self.changed = False
|
||||
self.check_mode = self.module.check_mode
|
||||
|
||||
def execute(self):
|
||||
try:
|
||||
self._execute()
|
||||
except Exception as e:
|
||||
self.module.fail_json(
|
||||
msg="unexpected error occurred: %s" % to_native(e),
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
self.module.exit_json(**self.result)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _execute(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
result = self._result
|
||||
|
||||
result['changed'] = self.changed
|
||||
|
||||
if self.module._diff:
|
||||
result['diff'] = self.diff
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def _result(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def diff(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def skip_if_check_mode(f):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if not self.check_mode:
|
||||
f(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
@staticmethod
|
||||
def trigger_change(f):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
f(self, *args, **kwargs)
|
||||
self.changed = True
|
||||
return wrapper
|
||||
|
||||
def _check_if_base_dir(self, path):
|
||||
base_dir = os.path.dirname(path) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
self.module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
|
||||
def _get_ssh_version(self):
|
||||
ssh_bin = self.module.get_bin_path('ssh')
|
||||
if not ssh_bin:
|
||||
return ""
|
||||
return parse_openssh_version(self.module.run_command([ssh_bin, '-V', '-q'])[2].strip())
|
||||
|
||||
@_restore_all_on_failure
|
||||
def _safe_secure_move(self, sources_and_destinations):
|
||||
"""Moves a list of files from 'source' to 'destination' and restores 'destination' from backup upon failure.
|
||||
If 'destination' does not already exist, then 'source' permissions are preserved to prevent
|
||||
exposing protected data ('atomic_move' uses the 'destination' base directory mask for
|
||||
permissions if 'destination' does not already exists).
|
||||
"""
|
||||
for source, destination in sources_and_destinations:
|
||||
if os.path.exists(destination):
|
||||
self.module.atomic_move(source, destination)
|
||||
else:
|
||||
self.module.preserved_copy(source, destination)
|
||||
|
||||
def _update_permissions(self, path):
|
||||
file_args = self.module.load_file_common_arguments(self.module.params)
|
||||
file_args['path'] = path
|
||||
|
||||
if not self.module.check_file_absent_if_check_mode(path):
|
||||
self.changed = self.module.set_fs_attributes_if_different(file_args, self.changed)
|
||||
else:
|
||||
self.changed = True
|
||||
|
||||
|
||||
class KeygenCommand(object):
|
||||
def __init__(self, module):
|
||||
self._bin_path = module.get_bin_path('ssh-keygen', True)
|
||||
self._run_command = module.run_command
|
||||
|
||||
def generate_certificate(self, certificate_path, identifier, options, pkcs11_provider, principals,
|
||||
serial_number, signature_algorithm, signing_key_path, type,
|
||||
time_parameters, use_agent, **kwargs):
|
||||
args = [self._bin_path, '-s', signing_key_path, '-P', '', '-I', identifier]
|
||||
|
||||
if options:
|
||||
for option in options:
|
||||
args.extend(['-O', option])
|
||||
if pkcs11_provider:
|
||||
args.extend(['-D', pkcs11_provider])
|
||||
if principals:
|
||||
args.extend(['-n', ','.join(principals)])
|
||||
if serial_number is not None:
|
||||
args.extend(['-z', str(serial_number)])
|
||||
if type == 'host':
|
||||
args.extend(['-h'])
|
||||
if use_agent:
|
||||
args.extend(['-U'])
|
||||
if time_parameters.validity_string:
|
||||
args.extend(['-V', time_parameters.validity_string])
|
||||
if signature_algorithm:
|
||||
args.extend(['-t', signature_algorithm])
|
||||
args.append(certificate_path)
|
||||
|
||||
return self._run_command(args, **kwargs)
|
||||
|
||||
def generate_keypair(self, private_key_path, size, type, comment, **kwargs):
|
||||
args = [
|
||||
self._bin_path,
|
||||
'-q',
|
||||
'-N', '',
|
||||
'-b', str(size),
|
||||
'-t', type,
|
||||
'-f', private_key_path,
|
||||
'-C', comment or ''
|
||||
]
|
||||
|
||||
# "y" must be entered in response to the "overwrite" prompt
|
||||
data = 'y' if os.path.exists(private_key_path) else None
|
||||
|
||||
return self._run_command(args, data=data, **kwargs)
|
||||
|
||||
def get_certificate_info(self, certificate_path, **kwargs):
|
||||
return self._run_command([self._bin_path, '-L', '-f', certificate_path], **kwargs)
|
||||
|
||||
def get_matching_public_key(self, private_key_path, **kwargs):
|
||||
return self._run_command([self._bin_path, '-P', '', '-y', '-f', private_key_path], **kwargs)
|
||||
|
||||
def get_private_key(self, private_key_path, **kwargs):
|
||||
return self._run_command([self._bin_path, '-l', '-f', private_key_path], **kwargs)
|
||||
|
||||
def update_comment(self, private_key_path, comment, **kwargs):
|
||||
if os.path.exists(private_key_path) and not os.access(private_key_path, os.W_OK):
|
||||
try:
|
||||
os.chmod(private_key_path, stat.S_IWUSR + stat.S_IRUSR)
|
||||
except (IOError, OSError) as e:
|
||||
raise e("The private key at %s is not writeable preventing a comment update" % private_key_path)
|
||||
|
||||
return self._run_command([self._bin_path, '-q', '-o', '-c', '-C', comment, '-f', private_key_path], **kwargs)
|
||||
|
||||
|
||||
class PrivateKey(object):
|
||||
def __init__(self, size, key_type, fingerprint):
|
||||
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,320 +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)
|
||||
|
||||
if self._permissions_changed() or self._permissions_changed(public_key=True):
|
||||
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 " +
|
||||
@@ -360,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):
|
||||
|
||||
677
plugins/module_utils/openssh/certificate.py
Normal file
677
plugins/module_utils/openssh/certificate.py
Normal file
@@ -0,0 +1,677 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
# Protocol References
|
||||
# -------------------
|
||||
# https://datatracker.ietf.org/doc/html/rfc4251
|
||||
# https://datatracker.ietf.org/doc/html/rfc4253
|
||||
# https://datatracker.ietf.org/doc/html/rfc5656
|
||||
# https://datatracker.ietf.org/doc/html/rfc8032
|
||||
# https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
|
||||
#
|
||||
# Inspired by:
|
||||
# ------------
|
||||
# https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/serialization/ssh.py
|
||||
# https://github.com/paramiko/paramiko/blob/master/paramiko/message.py
|
||||
|
||||
import abc
|
||||
import binascii
|
||||
import os
|
||||
from base64 import b64encode
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import convert_relative_to_datetime
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
|
||||
OpensshParser,
|
||||
_OpensshWriter,
|
||||
)
|
||||
|
||||
# See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
|
||||
_USER_TYPE = 1
|
||||
_HOST_TYPE = 2
|
||||
|
||||
_SSH_TYPE_STRINGS = {
|
||||
'rsa': b"ssh-rsa",
|
||||
'dsa': b"ssh-dss",
|
||||
'ecdsa-nistp256': b"ecdsa-sha2-nistp256",
|
||||
'ecdsa-nistp384': b"ecdsa-sha2-nistp384",
|
||||
'ecdsa-nistp521': b"ecdsa-sha2-nistp521",
|
||||
'ed25519': b"ssh-ed25519",
|
||||
}
|
||||
_CERT_SUFFIX_V01 = b"-cert-v01@openssh.com"
|
||||
|
||||
# See https://datatracker.ietf.org/doc/html/rfc5656#section-6.1
|
||||
_ECDSA_CURVE_IDENTIFIERS = {
|
||||
'ecdsa-nistp256': b'nistp256',
|
||||
'ecdsa-nistp384': b'nistp384',
|
||||
'ecdsa-nistp521': b'nistp521',
|
||||
}
|
||||
_ECDSA_CURVE_IDENTIFIERS_LOOKUP = {
|
||||
b'nistp256': 'ecdsa-nistp256',
|
||||
b'nistp384': 'ecdsa-nistp384',
|
||||
b'nistp521': 'ecdsa-nistp521',
|
||||
}
|
||||
|
||||
_ALWAYS = datetime(1970, 1, 1)
|
||||
_FOREVER = datetime.max
|
||||
|
||||
_CRITICAL_OPTIONS = (
|
||||
'force-command',
|
||||
'source-address',
|
||||
'verify-required',
|
||||
)
|
||||
|
||||
_DIRECTIVES = (
|
||||
'clear',
|
||||
'no-x11-forwarding',
|
||||
'no-agent-forwarding',
|
||||
'no-port-forwarding',
|
||||
'no-pty',
|
||||
'no-user-rc',
|
||||
)
|
||||
|
||||
_EXTENSIONS = (
|
||||
'permit-x11-forwarding',
|
||||
'permit-agent-forwarding',
|
||||
'permit-port-forwarding',
|
||||
'permit-pty',
|
||||
'permit-user-rc'
|
||||
)
|
||||
|
||||
if six.PY3:
|
||||
long = int
|
||||
|
||||
|
||||
class OpensshCertificateTimeParameters(object):
|
||||
def __init__(self, valid_from, valid_to):
|
||||
self._valid_from = self.to_datetime(valid_from)
|
||||
self._valid_to = self.to_datetime(valid_to)
|
||||
|
||||
if self._valid_from > self._valid_to:
|
||||
raise ValueError("Valid from: %s must not be greater than Valid to: %s" % (valid_from, valid_to))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, type(self)):
|
||||
return NotImplemented
|
||||
else:
|
||||
return self._valid_from == other._valid_from and self._valid_to == other._valid_to
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
@property
|
||||
def validity_string(self):
|
||||
if not (self._valid_from == _ALWAYS and self._valid_to == _FOREVER):
|
||||
return "%s:%s" % (
|
||||
self.valid_from(date_format='openssh'), self.valid_to(date_format='openssh')
|
||||
)
|
||||
return ""
|
||||
|
||||
def valid_from(self, date_format):
|
||||
return self.format_datetime(self._valid_from, date_format)
|
||||
|
||||
def valid_to(self, date_format):
|
||||
return self.format_datetime(self._valid_to, date_format)
|
||||
|
||||
def within_range(self, valid_at):
|
||||
if valid_at is not None:
|
||||
valid_at_datetime = self.to_datetime(valid_at)
|
||||
return self._valid_from <= valid_at_datetime <= self._valid_to
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def format_datetime(dt, date_format):
|
||||
if date_format in ('human_readable', 'openssh'):
|
||||
if dt == _ALWAYS:
|
||||
result = 'always'
|
||||
elif dt == _FOREVER:
|
||||
result = 'forever'
|
||||
else:
|
||||
result = dt.isoformat() if date_format == 'human_readable' else dt.strftime("%Y%m%d%H%M%S")
|
||||
elif date_format == 'timestamp':
|
||||
td = dt - _ALWAYS
|
||||
result = int((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6)
|
||||
else:
|
||||
raise ValueError("%s is not a valid format" % date_format)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def to_datetime(time_string_or_timestamp):
|
||||
try:
|
||||
if isinstance(time_string_or_timestamp, six.string_types):
|
||||
result = OpensshCertificateTimeParameters._time_string_to_datetime(time_string_or_timestamp.strip())
|
||||
elif isinstance(time_string_or_timestamp, (long, int)):
|
||||
result = OpensshCertificateTimeParameters._timestamp_to_datetime(time_string_or_timestamp)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Value must be of type (str, unicode, int, long) not %s" % type(time_string_or_timestamp)
|
||||
)
|
||||
except ValueError:
|
||||
raise
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _timestamp_to_datetime(timestamp):
|
||||
if timestamp == 0x0:
|
||||
result = _ALWAYS
|
||||
elif timestamp == 0xFFFFFFFFFFFFFFFF:
|
||||
result = _FOREVER
|
||||
else:
|
||||
try:
|
||||
result = datetime.utcfromtimestamp(timestamp)
|
||||
except OverflowError as e:
|
||||
raise ValueError
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _time_string_to_datetime(time_string):
|
||||
result = None
|
||||
if time_string == 'always':
|
||||
result = _ALWAYS
|
||||
elif time_string == 'forever':
|
||||
result = _FOREVER
|
||||
elif is_relative_time_string(time_string):
|
||||
result = convert_relative_to_datetime(time_string)
|
||||
else:
|
||||
for time_format in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
|
||||
try:
|
||||
result = datetime.strptime(time_string, time_format)
|
||||
except ValueError:
|
||||
pass
|
||||
if result is None:
|
||||
raise ValueError
|
||||
return result
|
||||
|
||||
|
||||
class OpensshCertificateOption(object):
|
||||
def __init__(self, option_type, name, data):
|
||||
if option_type not in ('critical', 'extension'):
|
||||
raise ValueError("type must be either 'critical' or 'extension'")
|
||||
|
||||
if not isinstance(name, six.string_types):
|
||||
raise TypeError("name must be a string not %s" % type(name))
|
||||
|
||||
if not isinstance(data, six.string_types):
|
||||
raise TypeError("data must be a string not %s" % type(data))
|
||||
|
||||
self._option_type = option_type
|
||||
self._name = name.lower()
|
||||
self._data = data
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, type(self)):
|
||||
return NotImplemented
|
||||
|
||||
return all([
|
||||
self._option_type == other._option_type,
|
||||
self._name == other._name,
|
||||
self._data == other._data,
|
||||
])
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self._option_type, self._name, self._data))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __str__(self):
|
||||
if self._data:
|
||||
return "%s=%s" % (self._name, self._data)
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._option_type
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, option_string):
|
||||
if not isinstance(option_string, six.string_types):
|
||||
raise ValueError("option_string must be a string not %s" % type(option_string))
|
||||
option_type = None
|
||||
|
||||
if ':' in option_string:
|
||||
option_type, value = option_string.strip().split(':', 1)
|
||||
if '=' in value:
|
||||
name, data = value.split('=', 1)
|
||||
else:
|
||||
name, data = value, ''
|
||||
elif '=' in option_string:
|
||||
name, data = option_string.strip().split('=', 1)
|
||||
else:
|
||||
name, data = option_string.strip(), ''
|
||||
|
||||
return cls(
|
||||
option_type=option_type or get_option_type(name.lower()),
|
||||
name=name,
|
||||
data=data
|
||||
)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class OpensshCertificateInfo:
|
||||
"""Encapsulates all certificate information which is signed by a CA key"""
|
||||
def __init__(self,
|
||||
nonce=None,
|
||||
serial=None,
|
||||
cert_type=None,
|
||||
key_id=None,
|
||||
principals=None,
|
||||
valid_after=None,
|
||||
valid_before=None,
|
||||
critical_options=None,
|
||||
extensions=None,
|
||||
reserved=None,
|
||||
signing_key=None):
|
||||
self.nonce = nonce
|
||||
self.serial = serial
|
||||
self._cert_type = cert_type
|
||||
self.key_id = key_id
|
||||
self.principals = principals
|
||||
self.valid_after = valid_after
|
||||
self.valid_before = valid_before
|
||||
self.critical_options = critical_options
|
||||
self.extensions = extensions
|
||||
self.reserved = reserved
|
||||
self.signing_key = signing_key
|
||||
|
||||
self.type_string = None
|
||||
|
||||
@property
|
||||
def cert_type(self):
|
||||
if self._cert_type == _USER_TYPE:
|
||||
return 'user'
|
||||
elif self._cert_type == _HOST_TYPE:
|
||||
return 'host'
|
||||
else:
|
||||
return ''
|
||||
|
||||
@cert_type.setter
|
||||
def cert_type(self, cert_type):
|
||||
if cert_type == 'user' or cert_type == _USER_TYPE:
|
||||
self._cert_type = _USER_TYPE
|
||||
elif cert_type == 'host' or cert_type == _HOST_TYPE:
|
||||
self._cert_type = _HOST_TYPE
|
||||
else:
|
||||
raise ValueError("%s is not a valid certificate type" % cert_type)
|
||||
|
||||
def signing_key_fingerprint(self):
|
||||
return fingerprint(self.signing_key)
|
||||
|
||||
@abc.abstractmethod
|
||||
def public_key_fingerprint(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def parse_public_numbers(self, parser):
|
||||
pass
|
||||
|
||||
|
||||
class OpensshRSACertificateInfo(OpensshCertificateInfo):
|
||||
def __init__(self, e=None, n=None, **kwargs):
|
||||
super(OpensshRSACertificateInfo, self).__init__(**kwargs)
|
||||
self.type_string = _SSH_TYPE_STRINGS['rsa'] + _CERT_SUFFIX_V01
|
||||
self.e = e
|
||||
self.n = n
|
||||
|
||||
# See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
def public_key_fingerprint(self):
|
||||
if any([self.e is None, self.n is None]):
|
||||
return b''
|
||||
|
||||
writer = _OpensshWriter()
|
||||
writer.string(_SSH_TYPE_STRINGS['rsa'])
|
||||
writer.mpint(self.e)
|
||||
writer.mpint(self.n)
|
||||
|
||||
return fingerprint(writer.bytes())
|
||||
|
||||
def parse_public_numbers(self, parser):
|
||||
self.e = parser.mpint()
|
||||
self.n = parser.mpint()
|
||||
|
||||
|
||||
class OpensshDSACertificateInfo(OpensshCertificateInfo):
|
||||
def __init__(self, p=None, q=None, g=None, y=None, **kwargs):
|
||||
super(OpensshDSACertificateInfo, self).__init__(**kwargs)
|
||||
self.type_string = _SSH_TYPE_STRINGS['dsa'] + _CERT_SUFFIX_V01
|
||||
self.p = p
|
||||
self.q = q
|
||||
self.g = g
|
||||
self.y = y
|
||||
|
||||
# See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
def public_key_fingerprint(self):
|
||||
if any([self.p is None, self.q is None, self.g is None, self.y is None]):
|
||||
return b''
|
||||
|
||||
writer = _OpensshWriter()
|
||||
writer.string(_SSH_TYPE_STRINGS['dsa'])
|
||||
writer.mpint(self.p)
|
||||
writer.mpint(self.q)
|
||||
writer.mpint(self.g)
|
||||
writer.mpint(self.y)
|
||||
|
||||
return fingerprint(writer.bytes())
|
||||
|
||||
def parse_public_numbers(self, parser):
|
||||
self.p = parser.mpint()
|
||||
self.q = parser.mpint()
|
||||
self.g = parser.mpint()
|
||||
self.y = parser.mpint()
|
||||
|
||||
|
||||
class OpensshECDSACertificateInfo(OpensshCertificateInfo):
|
||||
def __init__(self, curve=None, public_key=None, **kwargs):
|
||||
super(OpensshECDSACertificateInfo, self).__init__(**kwargs)
|
||||
self._curve = None
|
||||
if curve is not None:
|
||||
self.curve = curve
|
||||
|
||||
self.public_key = public_key
|
||||
|
||||
@property
|
||||
def curve(self):
|
||||
return self._curve
|
||||
|
||||
@curve.setter
|
||||
def curve(self, curve):
|
||||
if curve in _ECDSA_CURVE_IDENTIFIERS.values():
|
||||
self._curve = curve
|
||||
self.type_string = _SSH_TYPE_STRINGS[_ECDSA_CURVE_IDENTIFIERS_LOOKUP[curve]] + _CERT_SUFFIX_V01
|
||||
else:
|
||||
raise ValueError(
|
||||
"Curve must be one of %s" % (b','.join(list(_ECDSA_CURVE_IDENTIFIERS.values()))).decode('UTF-8')
|
||||
)
|
||||
|
||||
# See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
def public_key_fingerprint(self):
|
||||
if any([self.curve is None, self.public_key is None]):
|
||||
return b''
|
||||
|
||||
writer = _OpensshWriter()
|
||||
writer.string(_SSH_TYPE_STRINGS[_ECDSA_CURVE_IDENTIFIERS_LOOKUP[self.curve]])
|
||||
writer.string(self.curve)
|
||||
writer.string(self.public_key)
|
||||
|
||||
return fingerprint(writer.bytes())
|
||||
|
||||
def parse_public_numbers(self, parser):
|
||||
self.curve = parser.string()
|
||||
self.public_key = parser.string()
|
||||
|
||||
|
||||
class OpensshED25519CertificateInfo(OpensshCertificateInfo):
|
||||
def __init__(self, pk=None, **kwargs):
|
||||
super(OpensshED25519CertificateInfo, self).__init__(**kwargs)
|
||||
self.type_string = _SSH_TYPE_STRINGS['ed25519'] + _CERT_SUFFIX_V01
|
||||
self.pk = pk
|
||||
|
||||
def public_key_fingerprint(self):
|
||||
if self.pk is None:
|
||||
return b''
|
||||
|
||||
writer = _OpensshWriter()
|
||||
writer.string(_SSH_TYPE_STRINGS['ed25519'])
|
||||
writer.string(self.pk)
|
||||
|
||||
return fingerprint(writer.bytes())
|
||||
|
||||
def parse_public_numbers(self, parser):
|
||||
self.pk = parser.string()
|
||||
|
||||
|
||||
# See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
|
||||
class OpensshCertificate(object):
|
||||
"""Encapsulates a formatted OpenSSH certificate including signature and signing key"""
|
||||
def __init__(self, cert_info, signature):
|
||||
|
||||
self._cert_info = cert_info
|
||||
self.signature = signature
|
||||
|
||||
@classmethod
|
||||
def load(cls, path):
|
||||
if not os.path.exists(path):
|
||||
raise ValueError("%s is not a valid path." % path)
|
||||
|
||||
try:
|
||||
with open(path, 'rb') as cert_file:
|
||||
data = cert_file.read()
|
||||
except (IOError, OSError) as e:
|
||||
raise ValueError("%s cannot be opened for reading: %s" % (path, e))
|
||||
|
||||
try:
|
||||
format_identifier, b64_cert = data.split(b' ')[:2]
|
||||
cert = binascii.a2b_base64(b64_cert)
|
||||
except (binascii.Error, ValueError):
|
||||
raise ValueError("Certificate not in OpenSSH format")
|
||||
|
||||
for key_type, string in _SSH_TYPE_STRINGS.items():
|
||||
if format_identifier == string + _CERT_SUFFIX_V01:
|
||||
pub_key_type = key_type
|
||||
break
|
||||
else:
|
||||
raise ValueError("Invalid certificate format identifier: %s" % format_identifier)
|
||||
|
||||
parser = OpensshParser(cert)
|
||||
|
||||
if format_identifier != parser.string():
|
||||
raise ValueError("Certificate formats do not match")
|
||||
|
||||
try:
|
||||
cert_info = cls._parse_cert_info(pub_key_type, parser)
|
||||
signature = parser.string()
|
||||
except (TypeError, ValueError) as e:
|
||||
raise ValueError("Invalid certificate data: %s" % e)
|
||||
|
||||
if parser.remaining_bytes():
|
||||
raise ValueError(
|
||||
"%s bytes of additional data was not parsed while loading %s" % (parser.remaining_bytes(), path)
|
||||
)
|
||||
|
||||
return cls(
|
||||
cert_info=cert_info,
|
||||
signature=signature,
|
||||
)
|
||||
|
||||
@property
|
||||
def type_string(self):
|
||||
return to_text(self._cert_info.type_string)
|
||||
|
||||
@property
|
||||
def nonce(self):
|
||||
return self._cert_info.nonce
|
||||
|
||||
@property
|
||||
def public_key(self):
|
||||
return to_text(self._cert_info.public_key_fingerprint())
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
return self._cert_info.serial
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._cert_info.cert_type
|
||||
|
||||
@property
|
||||
def key_id(self):
|
||||
return to_text(self._cert_info.key_id)
|
||||
|
||||
@property
|
||||
def principals(self):
|
||||
return [to_text(p) for p in self._cert_info.principals]
|
||||
|
||||
@property
|
||||
def valid_after(self):
|
||||
return self._cert_info.valid_after
|
||||
|
||||
@property
|
||||
def valid_before(self):
|
||||
return self._cert_info.valid_before
|
||||
|
||||
@property
|
||||
def critical_options(self):
|
||||
return [
|
||||
OpensshCertificateOption('critical', to_text(n), to_text(d)) for n, d in self._cert_info.critical_options
|
||||
]
|
||||
|
||||
@property
|
||||
def extensions(self):
|
||||
return [OpensshCertificateOption('extension', to_text(n), to_text(d)) for n, d in self._cert_info.extensions]
|
||||
|
||||
@property
|
||||
def reserved(self):
|
||||
return self._cert_info.reserved
|
||||
|
||||
@property
|
||||
def signing_key(self):
|
||||
return to_text(self._cert_info.signing_key_fingerprint())
|
||||
|
||||
@property
|
||||
def signature_type(self):
|
||||
signature_data = OpensshParser.signature_data(self.signature)
|
||||
return to_text(signature_data['signature_type'])
|
||||
|
||||
@staticmethod
|
||||
def _parse_cert_info(pub_key_type, parser):
|
||||
cert_info = get_cert_info_object(pub_key_type)
|
||||
cert_info.nonce = parser.string()
|
||||
cert_info.parse_public_numbers(parser)
|
||||
cert_info.serial = parser.uint64()
|
||||
cert_info.cert_type = parser.uint32()
|
||||
cert_info.key_id = parser.string()
|
||||
cert_info.principals = parser.string_list()
|
||||
cert_info.valid_after = parser.uint64()
|
||||
cert_info.valid_before = parser.uint64()
|
||||
cert_info.critical_options = parser.option_list()
|
||||
cert_info.extensions = parser.option_list()
|
||||
cert_info.reserved = parser.string()
|
||||
cert_info.signing_key = parser.string()
|
||||
|
||||
return cert_info
|
||||
|
||||
def to_dict(self):
|
||||
time_parameters = OpensshCertificateTimeParameters(
|
||||
valid_from=self.valid_after,
|
||||
valid_to=self.valid_before
|
||||
)
|
||||
return {
|
||||
'type_string': self.type_string,
|
||||
'nonce': self.nonce,
|
||||
'serial': self.serial,
|
||||
'cert_type': self.type,
|
||||
'identifier': self.key_id,
|
||||
'principals': self.principals,
|
||||
'valid_after': time_parameters.valid_from(date_format='human_readable'),
|
||||
'valid_before': time_parameters.valid_to(date_format='human_readable'),
|
||||
'critical_options': [str(critical_option) for critical_option in self.critical_options],
|
||||
'extensions': [str(extension) for extension in self.extensions],
|
||||
'reserved': self.reserved,
|
||||
'public_key': self.public_key,
|
||||
'signing_key': self.signing_key,
|
||||
}
|
||||
|
||||
|
||||
def apply_directives(directives):
|
||||
if any(d not in _DIRECTIVES for d in directives):
|
||||
raise ValueError("directives must be one of %s" % ", ".join(_DIRECTIVES))
|
||||
|
||||
directive_to_option = {
|
||||
'no-x11-forwarding': OpensshCertificateOption('extension', 'permit-x11-forwarding', ''),
|
||||
'no-agent-forwarding': OpensshCertificateOption('extension', 'permit-agent-forwarding', ''),
|
||||
'no-port-forwarding': OpensshCertificateOption('extension', 'permit-port-forwarding', ''),
|
||||
'no-pty': OpensshCertificateOption('extension', 'permit-pty', ''),
|
||||
'no-user-rc': OpensshCertificateOption('extension', 'permit-user-rc', ''),
|
||||
}
|
||||
|
||||
if 'clear' in directives:
|
||||
return []
|
||||
else:
|
||||
return list(set(default_options()) - set(directive_to_option[d] for d in directives))
|
||||
|
||||
|
||||
def default_options():
|
||||
return [OpensshCertificateOption('extension', name, '') for name in _EXTENSIONS]
|
||||
|
||||
|
||||
def fingerprint(public_key):
|
||||
"""Generates a SHA256 hash and formats output to resemble ``ssh-keygen``"""
|
||||
h = sha256()
|
||||
h.update(public_key)
|
||||
return b'SHA256:' + b64encode(h.digest()).rstrip(b'=')
|
||||
|
||||
|
||||
def get_cert_info_object(key_type):
|
||||
if key_type == 'rsa':
|
||||
cert_info = OpensshRSACertificateInfo()
|
||||
elif key_type == 'dsa':
|
||||
cert_info = OpensshDSACertificateInfo()
|
||||
elif key_type in ('ecdsa-nistp256', 'ecdsa-nistp384', 'ecdsa-nistp521'):
|
||||
cert_info = OpensshECDSACertificateInfo()
|
||||
elif key_type == 'ed25519':
|
||||
cert_info = OpensshED25519CertificateInfo()
|
||||
else:
|
||||
raise ValueError("%s is not a valid key type" % key_type)
|
||||
|
||||
return cert_info
|
||||
|
||||
|
||||
def get_option_type(name):
|
||||
if name in _CRITICAL_OPTIONS:
|
||||
result = 'critical'
|
||||
elif name in _EXTENSIONS:
|
||||
result = 'extension'
|
||||
else:
|
||||
raise ValueError("%s is not a valid option. " % name +
|
||||
"Custom options must start with 'critical:' or 'extension:' to indicate type")
|
||||
return result
|
||||
|
||||
|
||||
def is_relative_time_string(time_string):
|
||||
return time_string.startswith("+") or time_string.startswith("-")
|
||||
|
||||
|
||||
def parse_option_list(option_list):
|
||||
critical_options = []
|
||||
directives = []
|
||||
extensions = []
|
||||
|
||||
for option in option_list:
|
||||
if option.lower() in _DIRECTIVES:
|
||||
directives.append(option.lower())
|
||||
else:
|
||||
option_object = OpensshCertificateOption.from_string(option)
|
||||
if option_object.type == 'critical':
|
||||
critical_options.append(option_object)
|
||||
else:
|
||||
extensions.append(option_object)
|
||||
|
||||
return critical_options, list(set(extensions + apply_directives(directives)))
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2020, Doug Stanley <doug+ansible@technologixllc.com>
|
||||
# Copyright: (c) 2020, Doug Stanley <doug+ansible@technologixllc.com>
|
||||
# Copyright: (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -18,7 +19,51 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from struct import Struct
|
||||
|
||||
from ansible.module_utils.six import PY3
|
||||
|
||||
# Protocol References
|
||||
# -------------------
|
||||
# https://datatracker.ietf.org/doc/html/rfc4251
|
||||
# https://datatracker.ietf.org/doc/html/rfc4253
|
||||
# https://datatracker.ietf.org/doc/html/rfc5656
|
||||
# https://datatracker.ietf.org/doc/html/rfc8032
|
||||
#
|
||||
# Inspired by:
|
||||
# ------------
|
||||
# https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/serialization/ssh.py
|
||||
# https://github.com/paramiko/paramiko/blob/master/paramiko/message.py
|
||||
|
||||
if PY3:
|
||||
long = int
|
||||
|
||||
# 0 (False) or 1 (True) encoded as a single byte
|
||||
_BOOLEAN = Struct(b'?')
|
||||
# Unsigned 8-bit integer in network-byte-order
|
||||
_UBYTE = Struct(b'!B')
|
||||
_UBYTE_MAX = 0xFF
|
||||
# Unsigned 32-bit integer in network-byte-order
|
||||
_UINT32 = Struct(b'!I')
|
||||
# Unsigned 32-bit little endian integer
|
||||
_UINT32_LE = Struct(b'<I')
|
||||
_UINT32_MAX = 0xFFFFFFFF
|
||||
# Unsigned 64-bit integer in network-byte-order
|
||||
_UINT64 = Struct(b'!Q')
|
||||
_UINT64_MAX = 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
|
||||
def any_in(sequence, *elements):
|
||||
return any(e in sequence for e in elements)
|
||||
|
||||
|
||||
def file_mode(path):
|
||||
if not os.path.exists(path):
|
||||
return 0o000
|
||||
return os.stat(path).st_mode & 0o777
|
||||
|
||||
|
||||
def parse_openssh_version(version_string):
|
||||
@@ -33,3 +78,326 @@ def parse_openssh_version(version_string):
|
||||
version = None
|
||||
|
||||
return version
|
||||
|
||||
|
||||
@contextmanager
|
||||
def secure_open(path, mode):
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
|
||||
try:
|
||||
yield fd
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def secure_write(path, mode, content):
|
||||
with secure_open(path, mode) as fd:
|
||||
os.write(fd, content)
|
||||
|
||||
|
||||
# See https://datatracker.ietf.org/doc/html/rfc4251#section-5 for SSH data types
|
||||
class OpensshParser(object):
|
||||
"""Parser for OpenSSH encoded objects"""
|
||||
BOOLEAN_OFFSET = 1
|
||||
UINT32_OFFSET = 4
|
||||
UINT64_OFFSET = 8
|
||||
|
||||
def __init__(self, data):
|
||||
if not isinstance(data, (bytes, bytearray)):
|
||||
raise TypeError("Data must be bytes-like not %s" % type(data))
|
||||
|
||||
self._data = memoryview(data) if PY3 else data
|
||||
self._pos = 0
|
||||
|
||||
def boolean(self):
|
||||
next_pos = self._check_position(self.BOOLEAN_OFFSET)
|
||||
|
||||
value = _BOOLEAN.unpack(self._data[self._pos:next_pos])[0]
|
||||
self._pos = next_pos
|
||||
return value
|
||||
|
||||
def uint32(self):
|
||||
next_pos = self._check_position(self.UINT32_OFFSET)
|
||||
|
||||
value = _UINT32.unpack(self._data[self._pos:next_pos])[0]
|
||||
self._pos = next_pos
|
||||
return value
|
||||
|
||||
def uint64(self):
|
||||
next_pos = self._check_position(self.UINT64_OFFSET)
|
||||
|
||||
value = _UINT64.unpack(self._data[self._pos:next_pos])[0]
|
||||
self._pos = next_pos
|
||||
return value
|
||||
|
||||
def string(self):
|
||||
length = self.uint32()
|
||||
|
||||
next_pos = self._check_position(length)
|
||||
|
||||
value = self._data[self._pos:next_pos]
|
||||
self._pos = next_pos
|
||||
# Cast to bytes is required as a memoryview slice is itself a memoryview
|
||||
return value if not PY3 else bytes(value)
|
||||
|
||||
def mpint(self):
|
||||
return self._big_int(self.string(), "big", signed=True)
|
||||
|
||||
def name_list(self):
|
||||
raw_string = self.string()
|
||||
return raw_string.decode('ASCII').split(',')
|
||||
|
||||
# Convenience function, but not an official data type from SSH
|
||||
def string_list(self):
|
||||
result = []
|
||||
raw_string = self.string()
|
||||
|
||||
if raw_string:
|
||||
parser = OpensshParser(raw_string)
|
||||
while parser.remaining_bytes():
|
||||
result.append(parser.string())
|
||||
|
||||
return result
|
||||
|
||||
# Convenience function, but not an official data type from SSH
|
||||
def option_list(self):
|
||||
result = []
|
||||
raw_string = self.string()
|
||||
|
||||
if raw_string:
|
||||
parser = OpensshParser(raw_string)
|
||||
|
||||
while parser.remaining_bytes():
|
||||
name = parser.string()
|
||||
data = parser.string()
|
||||
if data:
|
||||
# data is doubly-encoded
|
||||
data = OpensshParser(data).string()
|
||||
result.append((name, data))
|
||||
|
||||
return result
|
||||
|
||||
def seek(self, offset):
|
||||
self._pos = self._check_position(offset)
|
||||
|
||||
return self._pos
|
||||
|
||||
def remaining_bytes(self):
|
||||
return len(self._data) - self._pos
|
||||
|
||||
def _check_position(self, offset):
|
||||
if self._pos + offset > len(self._data):
|
||||
raise ValueError("Insufficient data remaining at position: %s" % self._pos)
|
||||
elif self._pos + offset < 0:
|
||||
raise ValueError("Position cannot be less than zero.")
|
||||
else:
|
||||
return self._pos + offset
|
||||
|
||||
@classmethod
|
||||
def signature_data(cls, signature_string):
|
||||
signature_data = {}
|
||||
|
||||
parser = cls(signature_string)
|
||||
signature_type = parser.string()
|
||||
signature_blob = parser.string()
|
||||
|
||||
blob_parser = cls(signature_blob)
|
||||
if signature_type in (b'ssh-rsa', b'rsa-sha2-256', b'rsa-sha2-512'):
|
||||
# https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
# https://datatracker.ietf.org/doc/html/rfc8332#section-3
|
||||
signature_data['s'] = cls._big_int(signature_blob, "big")
|
||||
elif signature_type == b'ssh-dss':
|
||||
# https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
signature_data['r'] = cls._big_int(signature_blob[:20], "big")
|
||||
signature_data['s'] = cls._big_int(signature_blob[20:], "big")
|
||||
elif signature_type in (b'ecdsa-sha2-nistp256', b'ecdsa-sha2-nistp384', b'ecdsa-sha2-nistp521'):
|
||||
# https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2
|
||||
signature_data['r'] = blob_parser.mpint()
|
||||
signature_data['s'] = blob_parser.mpint()
|
||||
elif signature_type == b'ssh-ed25519':
|
||||
# https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2
|
||||
signature_data['R'] = cls._big_int(signature_blob[:32], "little")
|
||||
signature_data['S'] = cls._big_int(signature_blob[32:], "little")
|
||||
else:
|
||||
raise ValueError("%s is not a valid signature type" % signature_type)
|
||||
|
||||
signature_data['signature_type'] = signature_type
|
||||
|
||||
return signature_data
|
||||
|
||||
@classmethod
|
||||
def _big_int(cls, raw_string, byte_order, signed=False):
|
||||
if byte_order not in ("big", "little"):
|
||||
raise ValueError("Byte_order must be one of (big, little) not %s" % byte_order)
|
||||
|
||||
if PY3:
|
||||
return int.from_bytes(raw_string, byte_order, signed=signed)
|
||||
|
||||
result = 0
|
||||
byte_length = len(raw_string)
|
||||
|
||||
if byte_length > 0:
|
||||
# Check sign-bit
|
||||
msb = raw_string[0] if byte_order == "big" else raw_string[-1]
|
||||
negative = bool(ord(msb) & 0x80)
|
||||
# Match pad value for two's complement
|
||||
pad = b'\xFF' if signed and negative else b'\x00'
|
||||
# The definition of ``mpint`` enforces that unnecessary bytes are not encoded so they are added back
|
||||
pad_length = (4 - byte_length % 4)
|
||||
if pad_length < 4:
|
||||
raw_string = pad * pad_length + raw_string if byte_order == "big" else raw_string + pad * pad_length
|
||||
byte_length += pad_length
|
||||
# Accumulate arbitrary precision integer bytes in the appropriate order
|
||||
if byte_order == "big":
|
||||
for i in range(0, byte_length, cls.UINT32_OFFSET):
|
||||
left_shift = result << cls.UINT32_OFFSET * 8
|
||||
result = left_shift + _UINT32.unpack(raw_string[i:i + cls.UINT32_OFFSET])[0]
|
||||
else:
|
||||
for i in range(byte_length, 0, -cls.UINT32_OFFSET):
|
||||
left_shift = result << cls.UINT32_OFFSET * 8
|
||||
result = left_shift + _UINT32_LE.unpack(raw_string[i - cls.UINT32_OFFSET:i])[0]
|
||||
# Adjust for two's complement
|
||||
if signed and negative:
|
||||
result -= 1 << (8 * byte_length)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class _OpensshWriter(object):
|
||||
"""Writes SSH encoded values to a bytes-like buffer
|
||||
|
||||
.. warning::
|
||||
This class is a private API and must not be exported outside of the openssh module_utils.
|
||||
It is not to be used to construct Openssh objects, but rather as a utility to assist
|
||||
in validating parsed material.
|
||||
"""
|
||||
def __init__(self, buffer=None):
|
||||
if buffer is not None:
|
||||
if not isinstance(buffer, (bytes, bytearray)):
|
||||
raise TypeError("Buffer must be a bytes-like object not %s" % type(buffer))
|
||||
else:
|
||||
buffer = bytearray()
|
||||
|
||||
self._buff = buffer
|
||||
|
||||
def boolean(self, value):
|
||||
if not isinstance(value, bool):
|
||||
raise TypeError("Value must be of type bool not %s" % type(value))
|
||||
|
||||
self._buff.extend(_BOOLEAN.pack(value))
|
||||
|
||||
return self
|
||||
|
||||
def uint32(self, value):
|
||||
if not isinstance(value, int):
|
||||
raise TypeError("Value must be of type int not %s" % type(value))
|
||||
if value < 0 or value > _UINT32_MAX:
|
||||
raise ValueError("Value must be a positive integer less than %s" % _UINT32_MAX)
|
||||
|
||||
self._buff.extend(_UINT32.pack(value))
|
||||
|
||||
return self
|
||||
|
||||
def uint64(self, value):
|
||||
if not isinstance(value, (long, int)):
|
||||
raise TypeError("Value must be of type (long, int) not %s" % type(value))
|
||||
if value < 0 or value > _UINT64_MAX:
|
||||
raise ValueError("Value must be a positive integer less than %s" % _UINT64_MAX)
|
||||
|
||||
self._buff.extend(_UINT64.pack(value))
|
||||
|
||||
return self
|
||||
|
||||
def string(self, value):
|
||||
if not isinstance(value, (bytes, bytearray)):
|
||||
raise TypeError("Value must be bytes-like not %s" % type(value))
|
||||
self.uint32(len(value))
|
||||
self._buff.extend(value)
|
||||
|
||||
return self
|
||||
|
||||
def mpint(self, value):
|
||||
if not isinstance(value, (int, long)):
|
||||
raise TypeError("Value must be of type (long, int) not %s" % type(value))
|
||||
|
||||
self.string(self._int_to_mpint(value))
|
||||
|
||||
return self
|
||||
|
||||
def name_list(self, value):
|
||||
if not isinstance(value, list):
|
||||
raise TypeError("Value must be a list of byte strings not %s" % type(value))
|
||||
|
||||
try:
|
||||
self.string(','.join(value).encode('ASCII'))
|
||||
except UnicodeEncodeError as e:
|
||||
raise ValueError("Name-list's must consist of US-ASCII characters: %s" % e)
|
||||
|
||||
return self
|
||||
|
||||
def string_list(self, value):
|
||||
if not isinstance(value, list):
|
||||
raise TypeError("Value must be a list of byte string not %s" % type(value))
|
||||
|
||||
writer = _OpensshWriter()
|
||||
for s in value:
|
||||
writer.string(s)
|
||||
|
||||
self.string(writer.bytes())
|
||||
|
||||
return self
|
||||
|
||||
def option_list(self, value):
|
||||
if not isinstance(value, list) or (value and not isinstance(value[0], tuple)):
|
||||
raise TypeError("Value must be a list of tuples")
|
||||
|
||||
writer = _OpensshWriter()
|
||||
for name, data in value:
|
||||
writer.string(name)
|
||||
# SSH option data is encoded twice though this behavior is not documented
|
||||
writer.string(_OpensshWriter().string(data).bytes() if data else bytes())
|
||||
|
||||
self.string(writer.bytes())
|
||||
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def _int_to_mpint(num):
|
||||
if PY3:
|
||||
byte_length = (num.bit_length() + 7) // 8
|
||||
try:
|
||||
result = num.to_bytes(byte_length, "big", signed=True)
|
||||
# Handles values which require \x00 or \xFF to pad sign-bit
|
||||
except OverflowError:
|
||||
result = num.to_bytes(byte_length + 1, "big", signed=True)
|
||||
else:
|
||||
result = bytes()
|
||||
# 0 and -1 are treated as special cases since they are used as sentinels for all other values
|
||||
if num == 0:
|
||||
result += b'\x00'
|
||||
elif num == -1:
|
||||
result += b'\xFF'
|
||||
elif num > 0:
|
||||
while num >> 32:
|
||||
result = _UINT32.pack(num & _UINT32_MAX) + result
|
||||
num = num >> 32
|
||||
# Pack last 4 bytes individually to discard insignificant bytes
|
||||
while num:
|
||||
result = _UBYTE.pack(num & _UBYTE_MAX) + result
|
||||
num = num >> 8
|
||||
# Zero pad final byte if most-significant bit is 1 as per mpint definition
|
||||
if ord(result[0]) & 0x80:
|
||||
result = b'\x00' + result
|
||||
else:
|
||||
while (num >> 32) < -1:
|
||||
result = _UINT32.pack(num & _UINT32_MAX) + result
|
||||
num = num >> 32
|
||||
while num < -1:
|
||||
result = _UBYTE.pack(num & _UBYTE_MAX) + result
|
||||
num = num >> 8
|
||||
if not ord(result[0]) & 0x80:
|
||||
result = b'\xFF' + result
|
||||
|
||||
return result
|
||||
|
||||
def bytes(self):
|
||||
return bytes(self._buff)
|
||||
|
||||
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:
|
||||
|
||||
@@ -308,7 +308,7 @@ EXAMPLES = r'''
|
||||
# - copy:
|
||||
# dest: /var/www/html/{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource'] }}
|
||||
# content: "{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}"
|
||||
# when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge['challenge_data']
|
||||
# when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge['challenge_data']
|
||||
#
|
||||
# Alternative way:
|
||||
#
|
||||
|
||||
@@ -143,7 +143,9 @@ import sys
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -241,7 +241,7 @@ output_json:
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native, to_bytes, to_text
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||
create_backend,
|
||||
|
||||
@@ -122,7 +122,9 @@ import os
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_bytes
|
||||
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:
|
||||
@@ -236,13 +237,17 @@ class CertificateSet(object):
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.certificates = set()
|
||||
self.certificate_by_issuer = dict()
|
||||
self.certificates_by_issuer = dict()
|
||||
self.certificate_by_cert = dict()
|
||||
|
||||
def _load_file(self, path):
|
||||
certs = load_PEM_list(self.module, path, fail_on_error=False)
|
||||
for cert in certs:
|
||||
self.certificates.add(cert)
|
||||
self.certificate_by_issuer[cert.cert.subject] = cert
|
||||
if cert.cert.subject not in self.certificates_by_issuer:
|
||||
self.certificates_by_issuer[cert.cert.subject] = []
|
||||
self.certificates_by_issuer[cert.cert.subject].append(cert)
|
||||
self.certificate_by_cert[cert.cert] = cert
|
||||
|
||||
def load(self, path):
|
||||
'''
|
||||
@@ -260,8 +265,8 @@ class CertificateSet(object):
|
||||
'''
|
||||
Search for the parent (issuer) of a certificate. Return ``None`` if none was found.
|
||||
'''
|
||||
potential_parent = self.certificate_by_issuer.get(cert.cert.issuer)
|
||||
if potential_parent is not None:
|
||||
potential_parents = self.certificates_by_issuer.get(cert.cert.issuer, [])
|
||||
for potential_parent in potential_parents:
|
||||
if is_parent(self.module, cert, potential_parent):
|
||||
return potential_parent
|
||||
return None
|
||||
@@ -274,6 +279,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 +327,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,10 +519,10 @@ 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._text import to_native, to_bytes
|
||||
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,
|
||||
|
||||
@@ -202,7 +202,7 @@ import datetime
|
||||
import time
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.ecs.api import (
|
||||
ecs_client_argument_spec,
|
||||
|
||||
@@ -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,13 +167,14 @@ 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
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_bytes
|
||||
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,
|
||||
@@ -189,7 +198,7 @@ try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
@@ -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:
|
||||
|
||||
@@ -350,12 +350,40 @@ STDERR = 2
|
||||
|
||||
# used to get <luks-name> out of lsblk output in format 'crypt <luks-name>'
|
||||
# regex takes care of any possible blank characters
|
||||
LUKS_NAME_REGEX = re.compile(r'\s*crypt\s+([^\s]*)\s*')
|
||||
LUKS_NAME_REGEX = re.compile(r'^crypt\s+([^\s]*)\s*$')
|
||||
# used to get </luks/device> out of lsblk output
|
||||
# in format 'device: </luks/device>'
|
||||
LUKS_DEVICE_REGEX = re.compile(r'\s*device:\s+([^\s]*)\s*')
|
||||
|
||||
|
||||
# See https://gitlab.com/cryptsetup/cryptsetup/-/wikis/LUKS-standard/on-disk-format.pdf
|
||||
LUKS_HEADER = b'LUKS\xba\xbe'
|
||||
LUKS_HEADER_L = 6
|
||||
# See https://gitlab.com/cryptsetup/LUKS2-docs/-/blob/master/luks2_doc_wip.pdf
|
||||
LUKS2_HEADER_OFFSETS = [0x4000, 0x8000, 0x10000, 0x20000, 0x40000, 0x80000, 0x100000, 0x200000, 0x400000]
|
||||
LUKS2_HEADER2 = b'SKUL\xba\xbe'
|
||||
|
||||
|
||||
def wipe_luks_headers(device):
|
||||
wipe_offsets = []
|
||||
with open(device, 'rb') as f:
|
||||
# f.seek(0)
|
||||
data = f.read(LUKS_HEADER_L)
|
||||
if data == LUKS_HEADER:
|
||||
wipe_offsets.append(0)
|
||||
for offset in LUKS2_HEADER_OFFSETS:
|
||||
f.seek(offset)
|
||||
data = f.read(LUKS_HEADER_L)
|
||||
if data == LUKS2_HEADER2:
|
||||
wipe_offsets.append(offset)
|
||||
|
||||
if wipe_offsets:
|
||||
with open(device, 'wb') as f:
|
||||
for offset in wipe_offsets:
|
||||
f.seek(offset)
|
||||
f.write(b'\x00\x00\x00\x00\x00\x00')
|
||||
|
||||
|
||||
class Handler(object):
|
||||
|
||||
def __init__(self, module):
|
||||
@@ -418,13 +446,11 @@ class CryptHandler(Handler):
|
||||
raise ValueError('Error while obtaining LUKS name for %s: %s'
|
||||
% (device, result[STDERR]))
|
||||
|
||||
m = LUKS_NAME_REGEX.search(result[STDOUT])
|
||||
|
||||
try:
|
||||
name = m.group(1)
|
||||
except AttributeError:
|
||||
name = None
|
||||
return name
|
||||
for line in result[STDOUT].splitlines(False):
|
||||
m = LUKS_NAME_REGEX.match(line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
def get_container_device_by_name(self, name):
|
||||
''' obtain device name based on the LUKS container name
|
||||
@@ -515,9 +541,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';
|
||||
@@ -785,6 +819,7 @@ def run_module():
|
||||
module = AnsibleModule(argument_spec=module_args,
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=mutually_exclusive)
|
||||
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
||||
|
||||
if module.params['device'] is not None:
|
||||
try:
|
||||
|
||||
@@ -20,7 +20,8 @@ requirements:
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Whether the host or user certificate should exist or not, taking action if the state is different from what is stated.
|
||||
- Whether the host or user certificate should exist or not, taking action if the state is different
|
||||
from what is stated.
|
||||
type: str
|
||||
default: "present"
|
||||
choices: [ 'present', 'absent' ]
|
||||
@@ -33,6 +34,7 @@ options:
|
||||
force:
|
||||
description:
|
||||
- Should the certificate be regenerated even if it already exists and is valid.
|
||||
- Equivalent to I(regenerate=always).
|
||||
type: bool
|
||||
default: false
|
||||
path:
|
||||
@@ -40,6 +42,46 @@ options:
|
||||
- Path of the file containing the certificate.
|
||||
type: path
|
||||
required: true
|
||||
regenerate:
|
||||
description:
|
||||
- When C(never) the task will fail if a certificate already exists at I(path) and is unreadable
|
||||
otherwise a new certificate will only be generated if there is no existing certificate.
|
||||
- When C(fail) the task will fail if a certificate already exists at I(path) and does not
|
||||
match the module's options.
|
||||
- When C(partial_idempotence) an existing certificate will be regenerated based on
|
||||
I(serial), I(signature_algorithm), I(type), I(valid_from), I(valid_to), I(valid_at), and I(principals).
|
||||
- When C(full_idempotence) I(identifier), I(options), I(public_key), and I(signing_key)
|
||||
are also considered when compared against an existing certificate.
|
||||
- C(always) is equivalent to I(force=true).
|
||||
type: str
|
||||
choices:
|
||||
- never
|
||||
- fail
|
||||
- partial_idempotence
|
||||
- full_idempotence
|
||||
- always
|
||||
default: partial_idempotence
|
||||
version_added: 1.8.0
|
||||
signature_algorithm:
|
||||
description:
|
||||
- As of OpenSSH 8.2 the SHA-1 signature algorithm for RSA keys has been disabled and C(ssh) will refuse
|
||||
host certificates signed with the SHA-1 algorithm. OpenSSH 8.1 made C(rsa-sha2-512) the default algorithm
|
||||
when acting as a CA and signing certificates with a RSA key. However, for OpenSSH versions less than 8.1
|
||||
the SHA-2 signature algorithms, C(rsa-sha2-256) or C(rsa-sha2-512), must be specified using this option
|
||||
if compatibility with newer C(ssh) clients is required. Conversely if hosts using OpenSSH version 8.2
|
||||
or greater must remain compatible with C(ssh) clients using OpenSSH less than 7.2, then C(ssh-rsa)
|
||||
can be used when generating host certificates (a corresponding change to the sshd_config to add C(ssh-rsa)
|
||||
to the C(CASignatureAlgorithms) keyword is also required).
|
||||
- Using any value for this option with a non-RSA I(signing_key) will cause this module to fail.
|
||||
- "Note: OpenSSH versions prior to 7.2 do not support SHA-2 signature algorithms for RSA keys and OpenSSH
|
||||
versions prior to 7.3 do not support SHA-2 signature algorithms for certificates."
|
||||
- See U(https://www.openssh.com/txt/release-8.2) for more information.
|
||||
type: str
|
||||
choices:
|
||||
- ssh-rsa
|
||||
- rsa-sha2-256
|
||||
- rsa-sha2-512
|
||||
version_added: 1.10.0
|
||||
signing_key:
|
||||
description:
|
||||
- The path to the private openssh key that is used for signing the public key in order to generate the certificate.
|
||||
@@ -215,423 +257,293 @@ info:
|
||||
|
||||
'''
|
||||
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import MINYEAR, MAXYEAR
|
||||
from distutils.version import LooseVersion
|
||||
from shutil import copy2, rmtree
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import convert_relative_to_datetime
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import parse_openssh_version
|
||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import (
|
||||
KeygenCommand,
|
||||
OpensshModule,
|
||||
PrivateKey,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.certificate import (
|
||||
OpensshCertificate,
|
||||
OpensshCertificateTimeParameters,
|
||||
parse_option_list,
|
||||
)
|
||||
|
||||
|
||||
class CertificateError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Certificate(object):
|
||||
|
||||
class Certificate(OpensshModule):
|
||||
def __init__(self, module):
|
||||
self.state = module.params['state']
|
||||
self.force = module.params['force']
|
||||
self.type = module.params['type']
|
||||
self.signing_key = module.params['signing_key']
|
||||
self.use_agent = module.params['use_agent']
|
||||
self.pkcs11_provider = module.params['pkcs11_provider']
|
||||
self.public_key = module.params['public_key']
|
||||
self.path = module.params['path']
|
||||
self.identifier = module.params['identifier']
|
||||
self.serial_number = module.params['serial_number']
|
||||
self.valid_from = module.params['valid_from']
|
||||
self.valid_to = module.params['valid_to']
|
||||
self.valid_at = module.params['valid_at']
|
||||
self.principals = module.params['principals']
|
||||
self.options = module.params['options']
|
||||
self.changed = False
|
||||
self.check_mode = module.check_mode
|
||||
self.cert_info = {}
|
||||
super(Certificate, self).__init__(module)
|
||||
self.ssh_keygen = KeygenCommand(self.module)
|
||||
|
||||
self.identifier = self.module.params['identifier'] or ""
|
||||
self.options = self.module.params['options'] or []
|
||||
self.path = self.module.params['path']
|
||||
self.pkcs11_provider = self.module.params['pkcs11_provider']
|
||||
self.principals = self.module.params['principals'] or []
|
||||
self.public_key = self.module.params['public_key']
|
||||
self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always'
|
||||
self.serial_number = self.module.params['serial_number']
|
||||
self.signature_algorithm = self.module.params['signature_algorithm']
|
||||
self.signing_key = self.module.params['signing_key']
|
||||
self.state = self.module.params['state']
|
||||
self.type = self.module.params['type']
|
||||
self.use_agent = self.module.params['use_agent']
|
||||
self.valid_at = self.module.params['valid_at']
|
||||
|
||||
self._check_if_base_dir(self.path)
|
||||
|
||||
if self.state == 'present':
|
||||
self._validate_parameters()
|
||||
|
||||
if self.options and self.type == "host":
|
||||
module.fail_json(msg="Options can only be used with user certificates.")
|
||||
self.data = None
|
||||
self.original_data = None
|
||||
if self._exists():
|
||||
self._load_certificate()
|
||||
|
||||
if self.valid_at:
|
||||
self.valid_at = self.valid_at.lstrip()
|
||||
self.time_parameters = None
|
||||
if self.state == 'present':
|
||||
self._set_time_parameters()
|
||||
|
||||
self.valid_from = self.valid_from.lstrip()
|
||||
self.valid_to = self.valid_to.lstrip()
|
||||
def _validate_parameters(self):
|
||||
for path in (self.public_key, self.signing_key):
|
||||
self._check_if_base_dir(path)
|
||||
|
||||
self.ssh_keygen = module.get_bin_path('ssh-keygen', True)
|
||||
if self.options and self.type == "host":
|
||||
self.module.fail_json(msg="Options can only be used with user certificates.")
|
||||
|
||||
def generate(self, module):
|
||||
if self.use_agent:
|
||||
self._use_agent_available()
|
||||
|
||||
if not self.is_valid(module, perms_required=False) or self.force:
|
||||
args = [
|
||||
self.ssh_keygen,
|
||||
'-s', self.signing_key
|
||||
]
|
||||
def _use_agent_available(self):
|
||||
ssh_version = self._get_ssh_version()
|
||||
if not ssh_version:
|
||||
self.module.fail_json(msg="Failed to determine ssh version")
|
||||
elif LooseVersion(ssh_version) < LooseVersion("7.6"):
|
||||
self.module.fail_json(
|
||||
msg="Signing with CA key in ssh agent requires ssh 7.6 or newer." +
|
||||
" Your version is: %s" % ssh_version
|
||||
)
|
||||
|
||||
if self.pkcs11_provider:
|
||||
args.extend(['-D', self.pkcs11_provider])
|
||||
def _exists(self):
|
||||
return os.path.exists(self.path)
|
||||
|
||||
if self.use_agent:
|
||||
args.extend(['-U'])
|
||||
|
||||
validity = ""
|
||||
|
||||
if not (self.valid_from == "always" and self.valid_to == "forever"):
|
||||
|
||||
if not self.valid_from == "always":
|
||||
timeobj = self.convert_to_datetime(module, self.valid_from)
|
||||
validity += (
|
||||
str(timeobj.year).zfill(4) +
|
||||
str(timeobj.month).zfill(2) +
|
||||
str(timeobj.day).zfill(2) +
|
||||
str(timeobj.hour).zfill(2) +
|
||||
str(timeobj.minute).zfill(2) +
|
||||
str(timeobj.second).zfill(2)
|
||||
)
|
||||
else:
|
||||
validity += "19700101010101"
|
||||
|
||||
validity += ":"
|
||||
|
||||
if self.valid_to == "forever":
|
||||
# on ssh-keygen versions that have the year 2038 bug this will cause the datetime to be 2038-01-19T04:14:07
|
||||
timeobj = datetime(MAXYEAR, 12, 31)
|
||||
else:
|
||||
timeobj = self.convert_to_datetime(module, self.valid_to)
|
||||
|
||||
validity += (
|
||||
str(timeobj.year).zfill(4) +
|
||||
str(timeobj.month).zfill(2) +
|
||||
str(timeobj.day).zfill(2) +
|
||||
str(timeobj.hour).zfill(2) +
|
||||
str(timeobj.minute).zfill(2) +
|
||||
str(timeobj.second).zfill(2)
|
||||
)
|
||||
|
||||
args.extend(["-V", validity])
|
||||
|
||||
if self.type == 'host':
|
||||
args.extend(['-h'])
|
||||
|
||||
if self.identifier:
|
||||
args.extend(['-I', self.identifier])
|
||||
else:
|
||||
args.extend(['-I', ""])
|
||||
|
||||
if self.serial_number is not None:
|
||||
args.extend(['-z', str(self.serial_number)])
|
||||
|
||||
if self.principals:
|
||||
args.extend(['-n', ','.join(self.principals)])
|
||||
|
||||
if self.options:
|
||||
for option in self.options:
|
||||
args.extend(['-O'])
|
||||
args.extend([option])
|
||||
|
||||
args.extend(['-P', ''])
|
||||
|
||||
try:
|
||||
temp_directory = tempfile.mkdtemp()
|
||||
copy2(self.public_key, temp_directory)
|
||||
args.extend([temp_directory + "/" + os.path.basename(self.public_key)])
|
||||
module.run_command(args, environ_update=dict(TZ="UTC"), check_rc=True)
|
||||
copy2(temp_directory + "/" + os.path.splitext(os.path.basename(self.public_key))[0] + "-cert.pub", self.path)
|
||||
rmtree(temp_directory, ignore_errors=True)
|
||||
proc = module.run_command([self.ssh_keygen, '-L', '-f', self.path])
|
||||
self.cert_info = proc[1].split()
|
||||
self.changed = True
|
||||
except Exception as e:
|
||||
try:
|
||||
self.remove()
|
||||
rmtree(temp_directory, ignore_errors=True)
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise CertificateError(exc)
|
||||
else:
|
||||
pass
|
||||
module.fail_json(msg="%s" % to_native(e))
|
||||
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if module.check_file_absent_if_check_mode(file_args['path']):
|
||||
self.changed = True
|
||||
elif module.set_fs_attributes_if_different(file_args, False):
|
||||
self.changed = True
|
||||
|
||||
def convert_to_datetime(self, module, timestring):
|
||||
|
||||
if self.is_relative(timestring):
|
||||
result = convert_relative_to_datetime(timestring)
|
||||
if result is None:
|
||||
module.fail_json(
|
||||
msg="'%s' is not a valid time format." % timestring)
|
||||
else:
|
||||
return result
|
||||
else:
|
||||
formats = ["%Y-%m-%d",
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y-%m-%dT%H:%M:%S",
|
||||
]
|
||||
for fmt in formats:
|
||||
try:
|
||||
return datetime.strptime(timestring, fmt)
|
||||
except ValueError:
|
||||
pass
|
||||
module.fail_json(msg="'%s' is not a valid time format" % timestring)
|
||||
|
||||
def is_relative(self, timestr):
|
||||
if timestr.startswith("+") or timestr.startswith("-"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_same_datetime(self, datetime_one, datetime_two):
|
||||
|
||||
# This function is for backwards compatibility only because .total_seconds() is new in python2.7
|
||||
def timedelta_total_seconds(time_delta):
|
||||
return (time_delta.microseconds + 0.0 + (time_delta.seconds + time_delta.days * 24 * 3600) * 10 ** 6) / 10 ** 6
|
||||
# try to use .total_ seconds() from python2.7
|
||||
def _load_certificate(self):
|
||||
try:
|
||||
return (datetime_one - datetime_two).total_seconds() == 0.0
|
||||
except AttributeError:
|
||||
return timedelta_total_seconds(datetime_one - datetime_two) == 0.0
|
||||
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 is_valid(self, module, perms_required=True):
|
||||
|
||||
def _check_state():
|
||||
return os.path.exists(self.path)
|
||||
|
||||
if _check_state():
|
||||
proc = module.run_command([self.ssh_keygen, '-L', '-f', self.path], environ_update=dict(TZ="UTC"), check_rc=False)
|
||||
if proc[0] != 0:
|
||||
return False
|
||||
self.cert_info = proc[1].split()
|
||||
principals = re.findall("(?<=Principals:)(.*)(?=Critical)", proc[1], re.S)[0].split()
|
||||
principals = list(map(str.strip, principals))
|
||||
if principals == ["(none)"]:
|
||||
principals = None
|
||||
cert_type = re.findall("( user | host )", proc[1])[0].strip()
|
||||
serial_number = re.search(r"Serial: (\d+)", proc[1]).group(1)
|
||||
validity = re.findall("(from (\\d{4}-\\d{2}-\\d{2}T\\d{2}(:\\d{2}){2}) to (\\d{4}-\\d{2}-\\d{2}T\\d{2}(:\\d{2}){2}))", proc[1])
|
||||
if validity:
|
||||
if validity[0][1]:
|
||||
cert_valid_from = self.convert_to_datetime(module, validity[0][1])
|
||||
if self.is_same_datetime(cert_valid_from, self.convert_to_datetime(module, "1970-01-01 01:01:01")):
|
||||
cert_valid_from = datetime(MINYEAR, 1, 1)
|
||||
else:
|
||||
cert_valid_from = datetime(MINYEAR, 1, 1)
|
||||
|
||||
if validity[0][3]:
|
||||
cert_valid_to = self.convert_to_datetime(module, validity[0][3])
|
||||
if self.is_same_datetime(cert_valid_to, self.convert_to_datetime(module, "2038-01-19 03:14:07")):
|
||||
cert_valid_to = datetime(MAXYEAR, 12, 31)
|
||||
else:
|
||||
cert_valid_to = datetime(MAXYEAR, 12, 31)
|
||||
else:
|
||||
cert_valid_from = datetime(MINYEAR, 1, 1)
|
||||
cert_valid_to = datetime(MAXYEAR, 12, 31)
|
||||
else:
|
||||
return False
|
||||
|
||||
def _check_perms(module):
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
return not module.set_fs_attributes_if_different(file_args, False)
|
||||
|
||||
def _check_serial_number():
|
||||
if self.serial_number is None:
|
||||
return True
|
||||
return self.serial_number == int(serial_number)
|
||||
|
||||
def _check_type():
|
||||
return self.type == cert_type
|
||||
|
||||
def _check_principals():
|
||||
if not principals or not self.principals:
|
||||
return self.principals == principals
|
||||
return set(self.principals) == set(principals)
|
||||
|
||||
def _check_validity(module):
|
||||
if self.valid_from == "always":
|
||||
earliest_time = datetime(MINYEAR, 1, 1)
|
||||
elif self.is_relative(self.valid_from):
|
||||
earliest_time = None
|
||||
else:
|
||||
earliest_time = self.convert_to_datetime(module, self.valid_from)
|
||||
|
||||
if self.valid_to == "forever":
|
||||
last_time = datetime(MAXYEAR, 12, 31)
|
||||
elif self.is_relative(self.valid_to):
|
||||
last_time = None
|
||||
else:
|
||||
last_time = self.convert_to_datetime(module, self.valid_to)
|
||||
|
||||
if earliest_time:
|
||||
if not self.is_same_datetime(earliest_time, cert_valid_from):
|
||||
return False
|
||||
if last_time:
|
||||
if not self.is_same_datetime(last_time, cert_valid_to):
|
||||
return False
|
||||
|
||||
if self.valid_at:
|
||||
if cert_valid_from <= self.convert_to_datetime(module, self.valid_at) <= cert_valid_to:
|
||||
return True
|
||||
|
||||
if earliest_time and last_time:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
if perms_required and not _check_perms(module):
|
||||
return False
|
||||
|
||||
return _check_type() and _check_principals() and _check_validity(module) and _check_serial_number()
|
||||
|
||||
def dump(self):
|
||||
|
||||
"""Serialize the object into a dictionary."""
|
||||
|
||||
def filter_keywords(arr, keywords):
|
||||
concated = []
|
||||
string = ""
|
||||
for word in arr:
|
||||
if word in keywords:
|
||||
concated.append(string)
|
||||
string = word
|
||||
else:
|
||||
string += " " + word
|
||||
concated.append(string)
|
||||
# drop the certificate path
|
||||
concated.pop(0)
|
||||
return concated
|
||||
|
||||
def format_cert_info():
|
||||
return filter_keywords(self.cert_info, [
|
||||
"Type:",
|
||||
"Public",
|
||||
"Signing",
|
||||
"Key",
|
||||
"Serial:",
|
||||
"Valid:",
|
||||
"Principals:",
|
||||
"Critical",
|
||||
"Extensions:"])
|
||||
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':
|
||||
result = {
|
||||
'changed': self.changed,
|
||||
'type': self.type,
|
||||
'filename': self.path,
|
||||
'info': format_cert_info(),
|
||||
}
|
||||
if self._should_generate():
|
||||
self._generate()
|
||||
self._update_permissions(self.path)
|
||||
else:
|
||||
result = {
|
||||
'changed': self.changed,
|
||||
}
|
||||
if self._exists():
|
||||
self._remove()
|
||||
|
||||
return result
|
||||
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 remove(self):
|
||||
"""Remove the resource from the filesystem."""
|
||||
def _is_fully_valid(self):
|
||||
return self._is_partially_valid() and all([
|
||||
self._compare_options() if self.original_data.type == 'user' else True,
|
||||
self.original_data.key_id == self.identifier,
|
||||
self.original_data.public_key == self._get_key_fingerprint(self.public_key),
|
||||
self.original_data.signing_key == self._get_key_fingerprint(self.signing_key),
|
||||
])
|
||||
|
||||
def _is_partially_valid(self):
|
||||
return all([
|
||||
set(self.original_data.principals) == set(self.principals),
|
||||
self.original_data.signature_type == self.signature_algorithm if self.signature_algorithm else True,
|
||||
self.original_data.serial == self.serial_number if self.serial_number is not None else True,
|
||||
self.original_data.type == self.type,
|
||||
self._compare_time_parameters(),
|
||||
])
|
||||
|
||||
def _compare_time_parameters(self):
|
||||
try:
|
||||
original_time_parameters = OpensshCertificateTimeParameters(
|
||||
valid_from=self.original_data.valid_after,
|
||||
valid_to=self.original_data.valid_before
|
||||
)
|
||||
except ValueError as e:
|
||||
return self.module.fail_json(msg=to_native(e))
|
||||
|
||||
return all([
|
||||
original_time_parameters == self.time_parameters,
|
||||
original_time_parameters.within_range(self.valid_at)
|
||||
])
|
||||
|
||||
def _compare_options(self):
|
||||
try:
|
||||
critical_options, extensions = parse_option_list(self.options)
|
||||
except ValueError as e:
|
||||
return self.module.fail_json(msg=to_native(e))
|
||||
|
||||
return all([
|
||||
set(self.original_data.critical_options) == set(critical_options),
|
||||
set(self.original_data.extensions) == set(extensions)
|
||||
])
|
||||
|
||||
def _get_key_fingerprint(self, path):
|
||||
private_key_content = self.ssh_keygen.get_private_key(path, check_rc=True)[1]
|
||||
return PrivateKey.from_string(private_key_content).fingerprint
|
||||
|
||||
@OpensshModule.trigger_change
|
||||
@OpensshModule.skip_if_check_mode
|
||||
def _generate(self):
|
||||
try:
|
||||
temp_certificate = self._generate_temp_certificate()
|
||||
self._safe_secure_move([(temp_certificate, self.path)])
|
||||
except OSError as e:
|
||||
self.module.fail_json(msg="Unable to write certificate to %s: %s" % (self.path, to_native(e)))
|
||||
|
||||
try:
|
||||
self.data = OpensshCertificate.load(self.path)
|
||||
except (TypeError, ValueError) as e:
|
||||
self.module.fail_json(msg="Unable to read new certificate: %s" % to_native(e))
|
||||
|
||||
def _generate_temp_certificate(self):
|
||||
key_copy = os.path.join(self.module.tmpdir, os.path.basename(self.public_key))
|
||||
|
||||
try:
|
||||
self.module.preserved_copy(self.public_key, key_copy)
|
||||
except OSError as e:
|
||||
self.module.fail_json(msg="Unable to stage temporary key: %s" % to_native(e))
|
||||
self.module.add_cleanup_file(key_copy)
|
||||
|
||||
self.ssh_keygen.generate_certificate(
|
||||
key_copy, self.identifier, self.options, self.pkcs11_provider, self.principals, self.serial_number,
|
||||
self.signature_algorithm, self.signing_key, self.type, self.time_parameters, self.use_agent,
|
||||
environ_update=dict(TZ="UTC"), check_rc=True
|
||||
)
|
||||
|
||||
temp_cert = os.path.splitext(key_copy)[0] + '-cert.pub'
|
||||
self.module.add_cleanup_file(temp_cert)
|
||||
|
||||
return temp_cert
|
||||
|
||||
@OpensshModule.trigger_change
|
||||
@OpensshModule.skip_if_check_mode
|
||||
def _remove(self):
|
||||
try:
|
||||
os.remove(self.path)
|
||||
self.changed = True
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise CertificateError(exc)
|
||||
else:
|
||||
pass
|
||||
except OSError as e:
|
||||
self.module.fail_json(msg="Unable to remove existing certificate: %s" % to_native(e))
|
||||
|
||||
@property
|
||||
def _result(self):
|
||||
if self.state != 'present':
|
||||
return {}
|
||||
|
||||
certificate_info = self.ssh_keygen.get_certificate_info(self.path)[1]
|
||||
|
||||
return {
|
||||
'type': self.type,
|
||||
'filename': self.path,
|
||||
'info': format_cert_info(certificate_info),
|
||||
}
|
||||
|
||||
@property
|
||||
def diff(self):
|
||||
return {
|
||||
'before': get_cert_dict(self.original_data),
|
||||
'after': get_cert_dict(self.data)
|
||||
}
|
||||
|
||||
|
||||
def format_cert_info(cert_info):
|
||||
result = []
|
||||
string = ""
|
||||
|
||||
for word in cert_info.split():
|
||||
if word in ("Type:", "Public", "Signing", "Key", "Serial:", "Valid:", "Principals:", "Critical", "Extensions:"):
|
||||
result.append(string)
|
||||
string = word
|
||||
else:
|
||||
string += " " + word
|
||||
result.append(string)
|
||||
# Drop the certificate path
|
||||
result.pop(0)
|
||||
return result
|
||||
|
||||
|
||||
def get_cert_dict(data):
|
||||
if data is None:
|
||||
return {}
|
||||
|
||||
result = data.to_dict()
|
||||
result.pop('nonce')
|
||||
result['signature_algorithm'] = data.signature_type
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
||||
force=dict(type='bool', default=False),
|
||||
type=dict(type='str', choices=['host', 'user']),
|
||||
signing_key=dict(type='path'),
|
||||
use_agent=dict(type='bool', default=False),
|
||||
pkcs11_provider=dict(type='str'),
|
||||
public_key=dict(type='path'),
|
||||
path=dict(type='path', required=True),
|
||||
identifier=dict(type='str'),
|
||||
options=dict(type='list', elements='str'),
|
||||
path=dict(type='path', required=True),
|
||||
pkcs11_provider=dict(type='str'),
|
||||
principals=dict(type='list', elements='str'),
|
||||
public_key=dict(type='path'),
|
||||
regenerate=dict(
|
||||
type='str',
|
||||
default='partial_idempotence',
|
||||
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
|
||||
),
|
||||
signature_algorithm=dict(type='str', choices=['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512']),
|
||||
signing_key=dict(type='path'),
|
||||
serial_number=dict(type='int'),
|
||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
||||
type=dict(type='str', choices=['host', 'user']),
|
||||
use_agent=dict(type='bool', default=False),
|
||||
valid_at=dict(type='str'),
|
||||
valid_from=dict(type='str'),
|
||||
valid_to=dict(type='str'),
|
||||
valid_at=dict(type='str'),
|
||||
principals=dict(type='list', elements='str'),
|
||||
options=dict(type='list', elements='str'),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
add_file_common_args=True,
|
||||
required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])],
|
||||
)
|
||||
|
||||
if module.params['use_agent']:
|
||||
ssh = module.get_bin_path('ssh', True)
|
||||
proc = module.run_command([ssh, '-Vq'])
|
||||
ssh_version_string = proc[2].strip()
|
||||
ssh_version = parse_openssh_version(ssh_version_string)
|
||||
if ssh_version is None:
|
||||
module.fail_json(msg="Failed to parse ssh version")
|
||||
elif LooseVersion(ssh_version) < LooseVersion("7.6"):
|
||||
module.fail_json(
|
||||
msg=(
|
||||
"Signing with CA key in ssh agent requires ssh 7.6 or newer."
|
||||
" Your version is: %s"
|
||||
) % ssh_version_string
|
||||
)
|
||||
|
||||
def isBaseDir(path):
|
||||
base_dir = os.path.dirname(path) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
if module.params['state'] == "present":
|
||||
isBaseDir(module.params['signing_key'])
|
||||
isBaseDir(module.params['public_key'])
|
||||
|
||||
isBaseDir(module.params['path'])
|
||||
|
||||
certificate = Certificate(module)
|
||||
|
||||
if certificate.state == 'present':
|
||||
|
||||
if module.check_mode:
|
||||
certificate.changed = module.params['force'] or not certificate.is_valid(module)
|
||||
else:
|
||||
try:
|
||||
certificate.generate(module)
|
||||
except Exception as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
else:
|
||||
|
||||
if module.check_mode:
|
||||
certificate.changed = os.path.exists(module.params['path'])
|
||||
if certificate.changed:
|
||||
certificate.cert_info = {}
|
||||
else:
|
||||
try:
|
||||
certificate.remove()
|
||||
except Exception as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
result = certificate.dump()
|
||||
module.exit_json(**result)
|
||||
Certificate(module).execute()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -122,6 +122,7 @@ notes:
|
||||
- In case the ssh key is broken or password protected, the module will fail.
|
||||
Set the I(force) option to C(yes) if you want to regenerate the keypair.
|
||||
- Supports C(check_mode).
|
||||
- In the case a custom C(mode), C(group), C(owner), or other file attribute is provided it will be applied to both key files.
|
||||
|
||||
extends_documentation_fragment: files
|
||||
'''
|
||||
@@ -177,7 +178,7 @@ public_key:
|
||||
description: The public key of the generated SSH private key.
|
||||
returned: changed or success
|
||||
type: str
|
||||
sample: ssh-rsa AAAAB3Nza(...omitted...)veL4E3Xcw== test_key
|
||||
sample: ssh-rsa AAAAB3Nza(...omitted...)veL4E3Xcw==
|
||||
comment:
|
||||
description: The comment of the generated key.
|
||||
returned: changed or success
|
||||
@@ -185,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 (
|
||||
@@ -217,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__':
|
||||
|
||||
@@ -236,7 +236,7 @@ csr:
|
||||
|
||||
import os
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr import (
|
||||
select_backend,
|
||||
|
||||
@@ -297,7 +297,7 @@ authority_cert_serial_number:
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
|
||||
@@ -115,7 +115,7 @@ csr:
|
||||
type: str
|
||||
'''
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr import (
|
||||
select_backend,
|
||||
|
||||
@@ -128,10 +128,10 @@ import re
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.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,
|
||||
|
||||
@@ -239,10 +239,10 @@ 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._text import to_bytes, to_native
|
||||
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,
|
||||
@@ -276,7 +276,7 @@ try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
|
||||
@@ -144,7 +144,7 @@ privatekey:
|
||||
import os
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||
load_file_if_exists,
|
||||
|
||||
@@ -195,7 +195,7 @@ private_data:
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
|
||||
@@ -68,7 +68,7 @@ EXAMPLES = r'''
|
||||
no_log: true # make sure that private key data is not accidentally revealed in logs!
|
||||
|
||||
- name: Update encrypted key when openssl_privatekey_pipe reported a change
|
||||
community.sops.encrypt_sops:
|
||||
community.sops.sops_encrypt:
|
||||
path: private_key.pem.sops
|
||||
content_text: "{{ output.privatekey }}"
|
||||
when: output is changed
|
||||
|
||||
@@ -182,10 +182,10 @@ publickey:
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native
|
||||
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,
|
||||
@@ -217,7 +217,7 @@ try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -157,7 +157,7 @@ public_data:
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -107,7 +108,7 @@ try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
@@ -139,7 +140,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
||||
load_privatekey,
|
||||
)
|
||||
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -107,7 +108,7 @@ try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError):
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
@@ -139,7 +140,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
||||
load_certificate,
|
||||
)
|
||||
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ certificate:
|
||||
|
||||
import os
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
select_backend,
|
||||
|
||||
@@ -377,7 +377,7 @@ ocsp_uri:
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
|
||||
@@ -125,7 +125,7 @@ certificate:
|
||||
|
||||
import os
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
select_backend,
|
||||
|
||||
@@ -367,10 +367,10 @@ import base64
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native, to_text
|
||||
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)
|
||||
@@ -664,9 +664,7 @@ class CRL(OpenSSLObject):
|
||||
revoked_cert = revoked_cert.revocation_date(entry['revocation_date'])
|
||||
if entry['issuer'] is not None:
|
||||
revoked_cert = revoked_cert.add_extension(
|
||||
x509.CertificateIssuer([
|
||||
cryptography_get_name(name, 'issuer') for name in entry['issuer']
|
||||
]),
|
||||
x509.CertificateIssuer(entry['issuer']),
|
||||
entry['issuer_critical']
|
||||
)
|
||||
if entry['reason'] is not None:
|
||||
@@ -689,9 +687,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 +832,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()
|
||||
|
||||
@@ -152,7 +152,7 @@ import base64
|
||||
import binascii
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
|
||||
@@ -60,7 +60,7 @@ from ansible.module_utils.six import (
|
||||
string_types,
|
||||
text_type,
|
||||
)
|
||||
from ansible.module_utils._text import to_native, to_text
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
|
||||
|
||||
@@ -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', '>=')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user