mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
605cf2631e | ||
|
|
ee196fd8a3 | ||
|
|
45b7aa797e | ||
|
|
32dab841d7 | ||
|
|
5b1816719f | ||
|
|
bd2270fb05 | ||
|
|
3f40795a98 | ||
|
|
73bc0f5de7 | ||
|
|
f832c0a4ac | ||
|
|
10579c8834 | ||
|
|
f1a6baadc7 | ||
|
|
5de50b9f91 | ||
|
|
cf0d2679aa | ||
|
|
2d388bf8d0 | ||
|
|
056a86fcae | ||
|
|
ebbfd7c56f | ||
|
|
91d98c4413 | ||
|
|
51b6bb210d | ||
|
|
94634a347d | ||
|
|
e5acd27c9b | ||
|
|
e6cd66df53 | ||
|
|
589e7c72ef | ||
|
|
ecbd44df22 | ||
|
|
4ab2ed8b77 | ||
|
|
eb8dabce84 | ||
|
|
c5df302faa | ||
|
|
a581f1ebcd | ||
|
|
78b27ffedb | ||
|
|
e735bdab60 | ||
|
|
5f1efb6f7e | ||
|
|
c68bfedbaa | ||
|
|
871a185ecb | ||
|
|
ed03841fd1 | ||
|
|
d6c0d53442 | ||
|
|
a2a7d94055 | ||
|
|
2a7e452cf8 | ||
|
|
74ae95038c | ||
|
|
57c364fe87 | ||
|
|
04958ece31 | ||
|
|
838bdd711b | ||
|
|
f644db3c79 | ||
|
|
24e7d07973 | ||
|
|
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 |
@@ -19,6 +19,11 @@ schedules:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- cron: 0 12 * * 0
|
||||
displayName: Weekly (old stable branches)
|
||||
always: true
|
||||
branches:
|
||||
include:
|
||||
- stable-*
|
||||
|
||||
variables:
|
||||
@@ -55,6 +60,17 @@ stages:
|
||||
test: 'devel/sanity/extra'
|
||||
- name: Units
|
||||
test: 'devel/units/1'
|
||||
- stage: Ansible_2_12
|
||||
displayName: Sanity & Units 2.12
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
targets:
|
||||
- name: Sanity
|
||||
test: '2.12/sanity/1'
|
||||
- name: Units
|
||||
test: '2.12/units/1'
|
||||
- stage: Ansible_2_11
|
||||
displayName: Sanity & Units 2.11
|
||||
dependsOn: []
|
||||
@@ -97,16 +113,12 @@ stages:
|
||||
parameters:
|
||||
testFormat: devel/linux/{0}/1
|
||||
targets:
|
||||
- name: CentOS 6
|
||||
test: centos6
|
||||
- name: CentOS 7
|
||||
test: centos7
|
||||
- name: CentOS 8
|
||||
test: centos8
|
||||
- name: Fedora 33
|
||||
test: fedora33
|
||||
- name: Fedora 34
|
||||
test: fedora34
|
||||
- name: Fedora 35
|
||||
test: fedora35
|
||||
- name: openSUSE 15 py2
|
||||
test: opensuse15py2
|
||||
- name: openSUSE 15 py3
|
||||
@@ -115,6 +127,24 @@ stages:
|
||||
test: ubuntu1804
|
||||
- name: Ubuntu 20.04
|
||||
test: ubuntu2004
|
||||
- stage: Docker_2_12
|
||||
displayName: Docker 2.12
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: 2.12/linux/{0}/1
|
||||
targets:
|
||||
- name: CentOS 6
|
||||
test: centos6
|
||||
- name: CentOS 8
|
||||
test: centos8
|
||||
- name: Fedora 33
|
||||
test: fedora33
|
||||
- name: openSUSE 15 py3
|
||||
test: opensuse15
|
||||
- name: Ubuntu 20.04
|
||||
test: ubuntu2004
|
||||
- stage: Docker_2_11
|
||||
displayName: Docker 2.11
|
||||
dependsOn: []
|
||||
@@ -131,12 +161,8 @@ stages:
|
||||
test: fedora32
|
||||
- name: openSUSE 15 py2
|
||||
test: opensuse15py2
|
||||
- name: openSUSE 15 py3
|
||||
test: opensuse15
|
||||
- name: Ubuntu 18.04
|
||||
test: ubuntu1804
|
||||
- name: Ubuntu 20.04
|
||||
test: ubuntu2004
|
||||
- stage: Docker_2_10
|
||||
displayName: Docker 2.10
|
||||
dependsOn: []
|
||||
@@ -147,22 +173,8 @@ 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,18 +187,10 @@ 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
|
||||
|
||||
@@ -203,12 +207,26 @@ stages:
|
||||
test: macos/11.1
|
||||
- name: RHEL 7.9
|
||||
test: rhel/7.9
|
||||
- name: RHEL 8.3
|
||||
test: rhel/8.3
|
||||
- name: RHEL 8.5
|
||||
test: rhel/8.5
|
||||
- name: FreeBSD 12.2
|
||||
test: freebsd/12.2
|
||||
- name: FreeBSD 13.0
|
||||
test: freebsd/13.0
|
||||
- stage: Remote_2_12
|
||||
displayName: Remote 2.12
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: 2.12/{0}/1
|
||||
targets:
|
||||
- name: macOS 11.1
|
||||
test: macos/11.1
|
||||
- name: RHEL 8.4
|
||||
test: rhel/8.4
|
||||
- name: FreeBSD 13.0
|
||||
test: freebsd/13.0
|
||||
- stage: Remote_2_11
|
||||
displayName: Remote 2.11
|
||||
dependsOn: []
|
||||
@@ -219,8 +237,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 +249,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
|
||||
@@ -259,7 +275,6 @@ stages:
|
||||
nameFormat: Python {0}
|
||||
testFormat: devel/cloud/{0}/1
|
||||
targets:
|
||||
- test: 2.6
|
||||
- test: 2.7
|
||||
- test: 3.5
|
||||
- test: 3.6
|
||||
@@ -267,6 +282,17 @@ stages:
|
||||
- test: 3.8
|
||||
- test: 3.9
|
||||
- test: "3.10"
|
||||
- stage: Cloud_2_12
|
||||
displayName: Cloud 2.12
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
nameFormat: Python {0}
|
||||
testFormat: 2.12/cloud/{0}/1
|
||||
targets:
|
||||
- test: 2.6
|
||||
- test: 3.9
|
||||
- stage: Cloud_2_11
|
||||
displayName: Cloud 2.11
|
||||
dependsOn: []
|
||||
@@ -304,20 +330,24 @@ stages:
|
||||
condition: succeededOrFailed()
|
||||
dependsOn:
|
||||
- Ansible_devel
|
||||
- Ansible_2_12
|
||||
- Ansible_2_11
|
||||
- Ansible_2_10
|
||||
- Ansible_2_9
|
||||
- Remote_devel
|
||||
- Docker_devel
|
||||
- Cloud_devel
|
||||
- Remote_2_12
|
||||
- Remote_2_11
|
||||
- Docker_2_11
|
||||
- Cloud_2_11
|
||||
- Remote_2_10
|
||||
- Docker_2_10
|
||||
- Cloud_2_10
|
||||
- Remote_2_9
|
||||
- Docker_devel
|
||||
- Docker_2_12
|
||||
- Docker_2_11
|
||||
- Docker_2_10
|
||||
- Docker_2_9
|
||||
- Cloud_devel
|
||||
- Cloud_2_12
|
||||
- Cloud_2_11
|
||||
- Cloud_2_10
|
||||
- Cloud_2_9
|
||||
jobs:
|
||||
- template: templates/coverage.yml
|
||||
|
||||
@@ -11,7 +11,7 @@ mkdir "${agent_temp_directory}/coverage/"
|
||||
|
||||
options=(--venv --venv-system-site-packages --color -v)
|
||||
|
||||
ansible-test coverage combine --export "${agent_temp_directory}/coverage/" "${options[@]}"
|
||||
ansible-test coverage combine --group-by command --export "${agent_temp_directory}/coverage/" "${options[@]}"
|
||||
|
||||
if ansible-test coverage analyze targets generate --help >/dev/null 2>&1; then
|
||||
# Only analyze coverage if the installed version of ansible-test supports it.
|
||||
|
||||
101
.azure-pipelines/scripts/publish-codecov.py
Executable file
101
.azure-pipelines/scripts/publish-codecov.py
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Upload code coverage reports to codecov.io.
|
||||
Multiple coverage files from multiple languages are accepted and aggregated after upload.
|
||||
Python coverage, as well as PowerShell and Python stubs can all be uploaded.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import typing as t
|
||||
import urllib.request
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CoverageFile:
|
||||
name: str
|
||||
path: pathlib.Path
|
||||
flags: t.List[str]
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Args:
|
||||
dry_run: bool
|
||||
path: pathlib.Path
|
||||
|
||||
|
||||
def parse_args() -> Args:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-n', '--dry-run', action='store_true')
|
||||
parser.add_argument('path', type=pathlib.Path)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Store arguments in a typed dataclass
|
||||
fields = dataclasses.fields(Args)
|
||||
kwargs = {field.name: getattr(args, field.name) for field in fields}
|
||||
|
||||
return Args(**kwargs)
|
||||
|
||||
|
||||
def process_files(directory: pathlib.Path) -> t.Tuple[CoverageFile, ...]:
|
||||
processed = []
|
||||
for file in directory.joinpath('reports').glob('coverage*.xml'):
|
||||
name = file.stem.replace('coverage=', '')
|
||||
|
||||
# Get flags from name
|
||||
flags = name.replace('-powershell', '').split('=') # Drop '-powershell' suffix
|
||||
flags = [flag if not flag.startswith('stub') else flag.split('-')[0] for flag in flags] # Remove "-01" from stub files
|
||||
|
||||
processed.append(CoverageFile(name, file, flags))
|
||||
|
||||
return tuple(processed)
|
||||
|
||||
|
||||
def upload_files(codecov_bin: pathlib.Path, files: t.Tuple[CoverageFile, ...], dry_run: bool = False) -> None:
|
||||
for file in files:
|
||||
cmd = [
|
||||
str(codecov_bin),
|
||||
'--name', file.name,
|
||||
'--file', str(file.path),
|
||||
]
|
||||
for flag in file.flags:
|
||||
cmd.extend(['--flags', flag])
|
||||
|
||||
if dry_run:
|
||||
print(f'DRY-RUN: Would run command: {cmd}')
|
||||
continue
|
||||
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
|
||||
def download_file(url: str, dest: pathlib.Path, flags: int, dry_run: bool = False) -> None:
|
||||
if dry_run:
|
||||
print(f'DRY-RUN: Would download {url} to {dest} and set mode to {flags:o}')
|
||||
return
|
||||
|
||||
with urllib.request.urlopen(url) as resp:
|
||||
with dest.open('w+b') as f:
|
||||
# Read data in chunks rather than all at once
|
||||
shutil.copyfileobj(resp, f, 64 * 1024)
|
||||
|
||||
dest.chmod(flags)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
url = 'https://ansible-ci-files.s3.amazonaws.com/codecov/linux/codecov'
|
||||
with tempfile.TemporaryDirectory(prefix='codecov-') as tmpdir:
|
||||
codecov_bin = pathlib.Path(tmpdir) / 'codecov'
|
||||
download_file(url, codecov_bin, 0o755, args.dry_run)
|
||||
|
||||
files = process_files(args.path)
|
||||
upload_files(codecov_bin, files, args.dry_run)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Upload code coverage reports to codecov.io.
|
||||
# Multiple coverage files from multiple languages are accepted and aggregated after upload.
|
||||
# Python coverage, as well as PowerShell and Python stubs can all be uploaded.
|
||||
|
||||
set -o pipefail -eu
|
||||
|
||||
output_path="$1"
|
||||
|
||||
curl --silent --show-error https://ansible-ci-files.s3.us-east-1.amazonaws.com/codecov/codecov.sh > codecov.sh
|
||||
|
||||
for file in "${output_path}"/reports/coverage*.xml; do
|
||||
name="${file}"
|
||||
name="${name##*/}" # remove path
|
||||
name="${name##coverage=}" # remove 'coverage=' prefix if present
|
||||
name="${name%.xml}" # remove '.xml' suffix
|
||||
|
||||
bash codecov.sh \
|
||||
-f "${file}" \
|
||||
-n "${name}" \
|
||||
-X coveragepy \
|
||||
-X gcov \
|
||||
-X fix \
|
||||
-X search \
|
||||
-X xcode \
|
||||
|| echo "Failed to upload code coverage report to codecov.io: ${file}"
|
||||
done
|
||||
@@ -12,4 +12,4 @@ if ! ansible-test --help >/dev/null 2>&1; then
|
||||
pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check
|
||||
fi
|
||||
|
||||
ansible-test coverage xml --stub --venv --venv-system-site-packages --color -v
|
||||
ansible-test coverage xml --group-by command --stub --venv --venv-system-site-packages --color -v
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
summaryFileLocation: "$(outputPath)/reports/$(pipelinesCoverage).xml"
|
||||
displayName: Publish to Azure Pipelines
|
||||
condition: gt(variables.coverageFileCount, 0)
|
||||
- bash: .azure-pipelines/scripts/publish-codecov.sh "$(outputPath)"
|
||||
- bash: .azure-pipelines/scripts/publish-codecov.py "$(outputPath)"
|
||||
displayName: Publish to codecov.io
|
||||
condition: gt(variables.coverageFileCount, 0)
|
||||
continueOnError: true
|
||||
|
||||
5
.github/patchback.yml
vendored
Normal file
5
.github/patchback.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
backport_branch_prefix: patchback/backports/
|
||||
backport_label_prefix: backport-
|
||||
target_branch_prefix: stable-
|
||||
...
|
||||
191
CHANGELOG.rst
191
CHANGELOG.rst
@@ -5,6 +5,197 @@ Community Crypto Release Notes
|
||||
.. contents:: Topics
|
||||
|
||||
|
||||
v2.0.2
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Documentation fix release. No actual code changes.
|
||||
|
||||
v2.0.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release with extra forward compatibility for newer versions of cryptography.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- acme_* modules - fix usage of ``fetch_url`` with changes in latest ansible-core ``devel`` branch (https://github.com/ansible-collections/community.crypto/pull/339).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- acme_certificate - avoid passing multiple certificates to ``cryptography``'s X.509 certificate loader when ``fullchain_dest`` is used (https://github.com/ansible-collections/community.crypto/pull/324).
|
||||
- get_certificate, openssl_csr_info, x509_certificate_info - add fallback code for extension parsing that works with cryptography 36.0.0 and newer. This code re-serializes de-serialized extensions and thus can return slightly different values if the extension in the original CSR resp. certificate was not canonicalized correctly. This code is currently used as a fallback if the existing code stops working, but we will switch it to be the main code in a future release (https://github.com/ansible-collections/community.crypto/pull/331).
|
||||
- luks_device - now also runs a built-in LUKS signature cleaner on ``state=absent`` to make sure that also the secondary LUKS2 header is wiped when older versions of wipefs are used (https://github.com/ansible-collections/community.crypto/issues/326, https://github.com/ansible-collections/community.crypto/pull/327).
|
||||
- openssl_pkcs12 - use new PKCS#12 deserialization infrastructure from cryptography 36.0.0 if available (https://github.com/ansible-collections/community.crypto/pull/302).
|
||||
|
||||
v2.0.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
A new major release of the ``community.crypto`` collection. The main changes are removal of the PyOpenSSL backends for almost all modules (``openssl_pkcs12`` being the only exception), and removal of the ``assertonly`` provider in the ``x509_certificate`` provider. There are also some other breaking changes which should improve the user interface/experience of this collection long-term.
|
||||
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- acme_certificate - the ``subject`` and ``issuer`` fields in in the ``select_chain`` entries are now more strictly validated (https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
- openssl_csr, openssl_csr_pipe - provide a new ``subject_ordered`` option if the order of the components in the subject is of importance (https://github.com/ansible-collections/community.crypto/issues/291, https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
- openssl_csr, openssl_csr_pipe - there is now stricter validation of the values of the ``subject`` option (https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
- openssl_privatekey_info - add ``check_consistency`` option to request private key consistency checks to be done (https://github.com/ansible-collections/community.crypto/pull/309).
|
||||
- x509_certificate, x509_certificate_pipe - add ``ignore_timestamps`` option which allows to enable idempotency for 'not before' and 'not after' options (https://github.com/ansible-collections/community.crypto/issues/295, https://github.com/ansible-collections/community.crypto/pull/317).
|
||||
- x509_crl - provide a new ``issuer_ordered`` option if the order of the components in the issuer is of importance (https://github.com/ansible-collections/community.crypto/issues/291, https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
- x509_crl - there is now stricter validation of the values of the ``issuer`` option (https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
|
||||
Breaking Changes / Porting Guide
|
||||
--------------------------------
|
||||
|
||||
- Adjust ``dirName`` text parsing and to text converting code to conform to `Sections 2 and 3 of RFC 4514 <https://datatracker.ietf.org/doc/html/rfc4514.html>`_. This is similar to how `cryptography handles this <https://cryptography.io/en/latest/x509/reference/#cryptography.x509.Name.rfc4514_string>`_ (https://github.com/ansible-collections/community.crypto/pull/274).
|
||||
- acme module utils - removing compatibility code (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- acme_* modules - removed vendored copy of the Python library ``ipaddress``. If you are using Python 2.x, please make sure to install the library (https://github.com/ansible-collections/community.crypto/pull/287).
|
||||
- compatibility module_utils - removed vendored copy of the Python library ``ipaddress`` (https://github.com/ansible-collections/community.crypto/pull/287).
|
||||
- crypto module utils - removing compatibility code (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- get_certificate, openssl_csr_info, x509_certificate_info - depending on the ``cryptography`` version used, the modules might not return the ASN.1 value for an extension as contained in the certificate respectively CSR, but a re-encoded version of it. This should usually be identical to the value contained in the source file, unless the value was malformed. For extensions not handled by C(cryptography) the value contained in the source file is always returned unaltered (https://github.com/ansible-collections/community.crypto/pull/318).
|
||||
- module_utils - removed various PyOpenSSL support functions and default backend values that are not needed for the openssl_pkcs12 module (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_csr, openssl_csr_pipe, x509_crl - the ``subject`` respectively ``issuer`` fields no longer ignore empty values, but instead fail when encountering them (https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
- openssl_privatekey_info - by default consistency checks are not run; they need to be explicitly requested by passing ``check_consistency=true`` (https://github.com/ansible-collections/community.crypto/pull/309).
|
||||
- x509_crl - for idempotency checks, the ``issuer`` order is ignored. If order is important, use the new ``issuer_ordered`` option (https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
|
||||
Deprecated Features
|
||||
-------------------
|
||||
|
||||
- acme_* modules - ACME version 1 is now deprecated and support for it will be removed in community.crypto 2.0.0 (https://github.com/ansible-collections/community.crypto/pull/288).
|
||||
|
||||
Removed Features (previously deprecated)
|
||||
----------------------------------------
|
||||
|
||||
- acme_* modules - the ``acme_directory`` option is now required (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- acme_* modules - the ``acme_version`` option is now required (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- acme_account_facts - the deprecated redirect has been removed. Use community.crypto.acme_account_info instead (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- acme_account_info - ``retrieve_orders=url_list`` no longer returns the return value ``orders``. Use the ``order_uris`` return value instead (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- crypto.info module utils - the deprecated redirect has been removed. Use ``crypto.pem`` instead (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- get_certificate - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_certificate - the deprecated redirect has been removed. Use community.crypto.x509_certificate instead (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- openssl_certificate_info - the deprecated redirect has been removed. Use community.crypto.x509_certificate_info instead (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- openssl_csr - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_csr and openssl_csr_pipe - ``version`` now only accepts the (default) value 1 (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- openssl_csr_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_csr_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_privatekey - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_privatekey_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_privatekey_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_publickey - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_publickey_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_signature - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_signature_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- x509_certificate - remove ``assertonly`` provider (https://github.com/ansible-collections/community.crypto/pull/289).
|
||||
- x509_certificate - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- x509_certificate_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- x509_certificate_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- cryptography backend - improve Unicode handling for Python 2 (https://github.com/ansible-collections/community.crypto/pull/313).
|
||||
- get_certificate - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- openssl_pkcs12 - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/296).
|
||||
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
|
||||
v1.9.4
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- acme_* modules - fix commands composed for OpenSSL backend to retrieve information on CSRs and certificates from stdin to use ``/dev/stdin`` instead of ``-``. This is needed for OpenSSL 1.0.1 and 1.0.2, apparently (https://github.com/ansible-collections/community.crypto/pull/279).
|
||||
- acme_challenge_cert_helper - only return exception when cryptography is not installed, not when a too old version of it is installed. This prevents Ansible's callback to crash (https://github.com/ansible-collections/community.crypto/pull/281).
|
||||
|
||||
v1.9.3
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssl_csr and openssl_csr_pipe - make sure that Unicode strings are used to compare strings with the cryptography backend. This fixes idempotency problems with non-ASCII letters on Python 2 (https://github.com/ansible-collections/community.crypto/issues/270, https://github.com/ansible-collections/community.crypto/pull/271).
|
||||
|
||||
v1.9.2
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release to fix the changelog. No other change compared to 1.9.0.
|
||||
|
||||
v1.9.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Accidental 1.9.1 release. Identical to 1.9.0.
|
||||
|
||||
v1.9.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular feature release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- get_certificate - added ``starttls`` option to retrieve certificates from servers which require clients to request an encrypted connection (https://github.com/ansible-collections/community.crypto/pull/264).
|
||||
- openssh_keypair - added ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- keypair_backend module utils - simplify code to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
- openssh_keypair - fixed ``cryptography`` backend to preserve original file permissions when regenerating a keypair requires existing files to be overwritten (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- openssh_keypair - fixed error handling to restore original keypair if regeneration fails (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- x509_crl - restore inherited function signature to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
|
||||
v1.8.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix and feature release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Avoid internal ansible-core module_utils in favor of equivalent public API available since at least Ansible 2.9 (https://github.com/ansible-collections/community.crypto/pull/253).
|
||||
- openssh certificate module utils - new module_utils for parsing OpenSSH certificates (https://github.com/ansible-collections/community.crypto/pull/246).
|
||||
- openssh_cert - added ``regenerate`` option to validate additional certificate parameters which trigger regeneration of an existing certificate (https://github.com/ansible-collections/community.crypto/pull/256).
|
||||
- openssh_cert - adding ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- openssh_cert - fixed certificate generation to restore original certificate if an error is encountered (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||
- openssh_keypair - fixed a bug that prevented custom file attributes being applied to public keys (https://github.com/ansible-collections/community.crypto/pull/257).
|
||||
|
||||
v1.7.1
|
||||
======
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Please note that this collection does **not** support Windows targets.
|
||||
|
||||
## Tested with Ansible
|
||||
|
||||
Tested with the current Ansible 2.9, ansible-base 2.10 and ansible-core 2.11 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
|
||||
Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11 and ansible-core 2.12 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
|
||||
|
||||
## External requirements
|
||||
|
||||
|
||||
@@ -479,3 +479,235 @@ releases:
|
||||
- 1.7.1.yml
|
||||
- 248-openssl_pkcs12-passphrase-fix.yml
|
||||
release_date: '2021-06-11'
|
||||
1.8.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssh_cert - fixed certificate generation to restore original certificate
|
||||
if an error is encountered (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||
- openssh_keypair - fixed a bug that prevented custom file attributes being
|
||||
applied to public keys (https://github.com/ansible-collections/community.crypto/pull/257).
|
||||
minor_changes:
|
||||
- Avoid internal ansible-core module_utils in favor of equivalent public API
|
||||
available since at least Ansible 2.9 (https://github.com/ansible-collections/community.crypto/pull/253).
|
||||
- openssh certificate module utils - new module_utils for parsing OpenSSH certificates
|
||||
(https://github.com/ansible-collections/community.crypto/pull/246).
|
||||
- openssh_cert - added ``regenerate`` option to validate additional certificate
|
||||
parameters which trigger regeneration of an existing certificate (https://github.com/ansible-collections/community.crypto/pull/256).
|
||||
- openssh_cert - adding ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||
release_summary: Regular bugfix and feature release.
|
||||
fragments:
|
||||
- 1.8.0.yml
|
||||
- 246-openssh-certificate-module-utils.yml
|
||||
- 255-openssh_cert-adding-diff-support.yml
|
||||
- 256-openssh_cert-adding-idempotency-option.yml
|
||||
- 257-openssh-keypair-fix-pubkey-permissions.yml
|
||||
- ansible-core-_text.yml
|
||||
release_date: '2021-08-10'
|
||||
1.9.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- keypair_backend module utils - simplify code to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
- openssh_keypair - fixed ``cryptography`` backend to preserve original file
|
||||
permissions when regenerating a keypair requires existing files to be overwritten
|
||||
(https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- openssh_keypair - fixed error handling to restore original keypair if regeneration
|
||||
fails (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
- x509_crl - restore inherited function signature to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||
minor_changes:
|
||||
- get_certificate - added ``starttls`` option to retrieve certificates from
|
||||
servers which require clients to request an encrypted connection (https://github.com/ansible-collections/community.crypto/pull/264).
|
||||
- openssh_keypair - added ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||
release_summary: Regular feature release.
|
||||
fragments:
|
||||
- 1.9.0.yml
|
||||
- 260-openssh_keypair-diff-support.yml
|
||||
- 263-sanity.yml
|
||||
- 264-get_certificate-add-starttls-option.yml
|
||||
release_date: '2021-08-30'
|
||||
1.9.1:
|
||||
changes:
|
||||
release_summary: Accidental 1.9.1 release. Identical to 1.9.0.
|
||||
release_date: '2021-08-30'
|
||||
1.9.2:
|
||||
changes:
|
||||
release_summary: Bugfix release to fix the changelog. No other change compared
|
||||
to 1.9.0.
|
||||
fragments:
|
||||
- 1.9.2.yml
|
||||
release_date: '2021-08-30'
|
||||
1.9.3:
|
||||
changes:
|
||||
bugfixes:
|
||||
- openssl_csr and openssl_csr_pipe - make sure that Unicode strings are used
|
||||
to compare strings with the cryptography backend. This fixes idempotency problems
|
||||
with non-ASCII letters on Python 2 (https://github.com/ansible-collections/community.crypto/issues/270,
|
||||
https://github.com/ansible-collections/community.crypto/pull/271).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.3.yml
|
||||
- 271-openssl_csr-utf8.yml
|
||||
release_date: '2021-09-14'
|
||||
1.9.4:
|
||||
changes:
|
||||
bugfixes:
|
||||
- acme_* modules - fix commands composed for OpenSSL backend to retrieve information
|
||||
on CSRs and certificates from stdin to use ``/dev/stdin`` instead of ``-``.
|
||||
This is needed for OpenSSL 1.0.1 and 1.0.2, apparently (https://github.com/ansible-collections/community.crypto/pull/279).
|
||||
- acme_challenge_cert_helper - only return exception when cryptography is not
|
||||
installed, not when a too old version of it is installed. This prevents Ansible's
|
||||
callback to crash (https://github.com/ansible-collections/community.crypto/pull/281).
|
||||
release_summary: Regular bugfix release.
|
||||
fragments:
|
||||
- 1.9.4.yml
|
||||
- 279-acme-openssl.yml
|
||||
- 282-acme_challenge_cert_helper-error.yml
|
||||
release_date: '2021-09-28'
|
||||
2.0.0:
|
||||
changes:
|
||||
breaking_changes:
|
||||
- Adjust ``dirName`` text parsing and to text converting code to conform to
|
||||
`Sections 2 and 3 of RFC 4514 <https://datatracker.ietf.org/doc/html/rfc4514.html>`_.
|
||||
This is similar to how `cryptography handles this <https://cryptography.io/en/latest/x509/reference/#cryptography.x509.Name.rfc4514_string>`_
|
||||
(https://github.com/ansible-collections/community.crypto/pull/274).
|
||||
- acme module utils - removing compatibility code (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- acme_* modules - removed vendored copy of the Python library ``ipaddress``.
|
||||
If you are using Python 2.x, please make sure to install the library (https://github.com/ansible-collections/community.crypto/pull/287).
|
||||
- compatibility module_utils - removed vendored copy of the Python library ``ipaddress``
|
||||
(https://github.com/ansible-collections/community.crypto/pull/287).
|
||||
- crypto module utils - removing compatibility code (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- get_certificate, openssl_csr_info, x509_certificate_info - depending on the
|
||||
``cryptography`` version used, the modules might not return the ASN.1 value
|
||||
for an extension as contained in the certificate respectively CSR, but a re-encoded
|
||||
version of it. This should usually be identical to the value contained in
|
||||
the source file, unless the value was malformed. For extensions not handled
|
||||
by C(cryptography) the value contained in the source file is always returned
|
||||
unaltered (https://github.com/ansible-collections/community.crypto/pull/318).
|
||||
- module_utils - removed various PyOpenSSL support functions and default backend
|
||||
values that are not needed for the openssl_pkcs12 module (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_csr, openssl_csr_pipe, x509_crl - the ``subject`` respectively ``issuer``
|
||||
fields no longer ignore empty values, but instead fail when encountering them
|
||||
(https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
- openssl_privatekey_info - by default consistency checks are not run; they
|
||||
need to be explicitly requested by passing ``check_consistency=true`` (https://github.com/ansible-collections/community.crypto/pull/309).
|
||||
- x509_crl - for idempotency checks, the ``issuer`` order is ignored. If order
|
||||
is important, use the new ``issuer_ordered`` option (https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
bugfixes:
|
||||
- cryptography backend - improve Unicode handling for Python 2 (https://github.com/ansible-collections/community.crypto/pull/313).
|
||||
- get_certificate - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release
|
||||
(https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
- openssl_pkcs12 - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/296).
|
||||
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release
|
||||
(https://github.com/ansible-collections/community.crypto/pull/294).
|
||||
deprecated_features:
|
||||
- acme_* modules - ACME version 1 is now deprecated and support for it will
|
||||
be removed in community.crypto 2.0.0 (https://github.com/ansible-collections/community.crypto/pull/288).
|
||||
minor_changes:
|
||||
- acme_certificate - the ``subject`` and ``issuer`` fields in in the ``select_chain``
|
||||
entries are now more strictly validated (https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
- openssl_csr, openssl_csr_pipe - provide a new ``subject_ordered`` option if
|
||||
the order of the components in the subject is of importance (https://github.com/ansible-collections/community.crypto/issues/291,
|
||||
https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
- openssl_csr, openssl_csr_pipe - there is now stricter validation of the values
|
||||
of the ``subject`` option (https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
- openssl_privatekey_info - add ``check_consistency`` option to request private
|
||||
key consistency checks to be done (https://github.com/ansible-collections/community.crypto/pull/309).
|
||||
- x509_certificate, x509_certificate_pipe - add ``ignore_timestamps`` option
|
||||
which allows to enable idempotency for 'not before' and 'not after' options
|
||||
(https://github.com/ansible-collections/community.crypto/issues/295, https://github.com/ansible-collections/community.crypto/pull/317).
|
||||
- x509_crl - provide a new ``issuer_ordered`` option if the order of the components
|
||||
in the issuer is of importance (https://github.com/ansible-collections/community.crypto/issues/291,
|
||||
https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
- x509_crl - there is now stricter validation of the values of the ``issuer``
|
||||
option (https://github.com/ansible-collections/community.crypto/pull/316).
|
||||
release_summary: 'A new major release of the ``community.crypto`` collection.
|
||||
The main changes are removal of the PyOpenSSL backends for almost all modules
|
||||
(``openssl_pkcs12`` being the only exception), and removal of the ``assertonly``
|
||||
provider in the ``x509_certificate`` provider. There are also some other breaking
|
||||
changes which should improve the user interface/experience of this collection
|
||||
long-term.
|
||||
|
||||
'
|
||||
removed_features:
|
||||
- acme_* modules - the ``acme_directory`` option is now required (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- acme_* modules - the ``acme_version`` option is now required (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- acme_account_facts - the deprecated redirect has been removed. Use community.crypto.acme_account_info
|
||||
instead (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- acme_account_info - ``retrieve_orders=url_list`` no longer returns the return
|
||||
value ``orders``. Use the ``order_uris`` return value instead (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- crypto.info module utils - the deprecated redirect has been removed. Use ``crypto.pem``
|
||||
instead (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- get_certificate - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_certificate - the deprecated redirect has been removed. Use community.crypto.x509_certificate
|
||||
instead (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- openssl_certificate_info - the deprecated redirect has been removed. Use community.crypto.x509_certificate_info
|
||||
instead (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- openssl_csr - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_csr and openssl_csr_pipe - ``version`` now only accepts the (default)
|
||||
value 1 (https://github.com/ansible-collections/community.crypto/pull/290).
|
||||
- openssl_csr_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_csr_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_privatekey - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_privatekey_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_privatekey_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_publickey - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_publickey_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_signature - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- openssl_signature_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- x509_certificate - remove ``assertonly`` provider (https://github.com/ansible-collections/community.crypto/pull/289).
|
||||
- x509_certificate - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- x509_certificate_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
- x509_certificate_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
|
||||
fragments:
|
||||
- 2.0.0.yml
|
||||
- 273-pyopenssl-removal.yml
|
||||
- 274-dirname-rfc4514.yml
|
||||
- 287-remove-ipaddress.yml
|
||||
- 288-depecate-acme-v1.yml
|
||||
- 289-assertonly-removed.yml
|
||||
- 290-remove-deprecations.yml
|
||||
- 294-cryptography-35.0.0.yml
|
||||
- 296-openssl_pkcs12-cryptography-35.yml
|
||||
- 309-openssl_privatekey_info-consistency.yml
|
||||
- 313-unicode-names.yml
|
||||
- 315-ordered-names.yml
|
||||
- 317-ignore-timestamps.yml
|
||||
- 318-extension-value-note.yml
|
||||
release_date: '2021-11-01'
|
||||
2.0.1:
|
||||
changes:
|
||||
bugfixes:
|
||||
- acme_certificate - avoid passing multiple certificates to ``cryptography``'s
|
||||
X.509 certificate loader when ``fullchain_dest`` is used (https://github.com/ansible-collections/community.crypto/pull/324).
|
||||
- get_certificate, openssl_csr_info, x509_certificate_info - add fallback code
|
||||
for extension parsing that works with cryptography 36.0.0 and newer. This
|
||||
code re-serializes de-serialized extensions and thus can return slightly different
|
||||
values if the extension in the original CSR resp. certificate was not canonicalized
|
||||
correctly. This code is currently used as a fallback if the existing code
|
||||
stops working, but we will switch it to be the main code in a future release
|
||||
(https://github.com/ansible-collections/community.crypto/pull/331).
|
||||
- luks_device - now also runs a built-in LUKS signature cleaner on ``state=absent``
|
||||
to make sure that also the secondary LUKS2 header is wiped when older versions
|
||||
of wipefs are used (https://github.com/ansible-collections/community.crypto/issues/326,
|
||||
https://github.com/ansible-collections/community.crypto/pull/327).
|
||||
- openssl_pkcs12 - use new PKCS#12 deserialization infrastructure from cryptography
|
||||
36.0.0 if available (https://github.com/ansible-collections/community.crypto/pull/302).
|
||||
minor_changes:
|
||||
- acme_* modules - fix usage of ``fetch_url`` with changes in latest ansible-core
|
||||
``devel`` branch (https://github.com/ansible-collections/community.crypto/pull/339).
|
||||
release_summary: Bugfix release with extra forward compatibility for newer versions
|
||||
of cryptography.
|
||||
fragments:
|
||||
- 2.0.1.yml
|
||||
- 302-openssl_pkcs12-cryptography-36.0.0.yml
|
||||
- 324-acme_certificate-fullchain.yml
|
||||
- 327-luks_device-wipe.yml
|
||||
- 331-cryptography-extensions.yml
|
||||
- fetch_url-devel.yml
|
||||
release_date: '2021-11-22'
|
||||
2.0.2:
|
||||
changes:
|
||||
release_summary: Documentation fix release. No actual code changes.
|
||||
fragments:
|
||||
- 2.0.2.yml
|
||||
release_date: '2021-12-20'
|
||||
|
||||
@@ -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.1
|
||||
version: 2.0.2
|
||||
readme: README.md
|
||||
authors:
|
||||
- Ansible (github.com/ansible)
|
||||
|
||||
@@ -12,20 +12,19 @@ action_groups:
|
||||
plugin_routing:
|
||||
modules:
|
||||
acme_account_facts:
|
||||
deprecation:
|
||||
tombstone:
|
||||
removal_version: 2.0.0
|
||||
warning_text: The 'community.crypto.acme_account_facts' module has been renamed to 'community.crypto.acme_account_info'.
|
||||
openssl_certificate:
|
||||
deprecation:
|
||||
tombstone:
|
||||
removal_version: 2.0.0
|
||||
warning_text: The 'community.crypto.openssl_certificate' module has been renamed to 'community.crypto.x509_certificate'
|
||||
openssl_certificate_info:
|
||||
deprecation:
|
||||
tombstone:
|
||||
removal_version: 2.0.0
|
||||
warning_text: The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_certificate_info'
|
||||
module_utils:
|
||||
crypto.identify:
|
||||
redirect: community.crypto.crypto.pem
|
||||
deprecation:
|
||||
tombstone:
|
||||
removal_version: 2.0.0
|
||||
warning_text: The 'crypto/identify.py' module_utils has been renamed 'crypto/pem.py'. Please update your imports
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ notes:
|
||||
principle be used with any CA providing an ACME endpoint, such as
|
||||
L(Buypass Go SSL,https://www.buypass.com/ssl/products/acme)."
|
||||
requirements:
|
||||
- python >= 2.6
|
||||
- either openssl or L(cryptography,https://cryptography.io/) >= 1.5
|
||||
- ipaddress
|
||||
options:
|
||||
account_key_src:
|
||||
description:
|
||||
@@ -33,7 +33,7 @@ options:
|
||||
key."
|
||||
- "Private keys can be created with the
|
||||
M(community.crypto.openssl_privatekey) or M(community.crypto.openssl_privatekey_pipe)
|
||||
modules. If the requisites (pyOpenSSL or cryptography) are not available,
|
||||
modules. If the requisite (cryptography) is not available,
|
||||
keys can also be created directly with the C(openssl) command line tool:
|
||||
RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys
|
||||
can be created with C(openssl ecparam -genkey ...). Any other tool creating
|
||||
@@ -74,9 +74,9 @@ options:
|
||||
- "The ACME version of the endpoint."
|
||||
- "Must be C(1) for the classic Let's Encrypt and Buypass ACME endpoints,
|
||||
or C(2) for standardized ACME v2 endpoints."
|
||||
- "The default value is C(1). Note that in community.crypto 2.0.0, this
|
||||
option B(will be required) and will no longer have a default."
|
||||
- "Please also note that we will deprecate ACME v1 support eventually."
|
||||
- "The value C(1) is deprecated since community.crypto 2.0.0 and will be
|
||||
removed from community.crypto 3.0.0."
|
||||
required: true
|
||||
type: int
|
||||
choices: [ 1, 2 ]
|
||||
acme_directory:
|
||||
@@ -86,22 +86,12 @@ options:
|
||||
- "For safety reasons the default is set to the Let's Encrypt staging
|
||||
server (for the ACME v1 protocol). This will create technically correct,
|
||||
but untrusted certificates."
|
||||
- "The default value is C(https://acme-staging.api.letsencrypt.org/directory).
|
||||
Note that in community.crypto 2.0.0, this option B(will be required) and
|
||||
will no longer have a default. Note that the default is the Let's Encrypt
|
||||
staging server for the ACME v1 protocol, which is deprecated and will
|
||||
be disabled in May 2021 (see
|
||||
L(here,https://community.letsencrypt.org/t/end-of-life-plan-for-acmev1/88430/7)
|
||||
for details)."
|
||||
- "For Let's Encrypt, all staging endpoints can be found here:
|
||||
U(https://letsencrypt.org/docs/staging-environment/). For Buypass, all
|
||||
endpoints can be found here:
|
||||
U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)"
|
||||
- "For B(Let's Encrypt), the production directory URL for ACME v2 is
|
||||
U(https://acme-v02.api.letsencrypt.org/directory).
|
||||
(The production directory URL for ACME v1 is
|
||||
U(https://acme-v01.api.letsencrypt.org/directory) and will be
|
||||
disabled in July 2021.)"
|
||||
U(https://acme-v02.api.letsencrypt.org/directory)."
|
||||
- "For B(Buypass), the production directory URL for ACME v2 and v1 is
|
||||
U(https://api.buypass.com/acme/directory)."
|
||||
- "For B(ZeroSSL), the production directory URL for ACME v2 is
|
||||
@@ -113,6 +103,7 @@ options:
|
||||
L(create an issue,https://github.com/ansible-collections/community.crypto/issues/new/choose)
|
||||
to help us supporting it. Feedback that an ACME server not mentioned does work
|
||||
is also appreciated."
|
||||
required: true
|
||||
type: str
|
||||
validate_certs:
|
||||
description:
|
||||
|
||||
@@ -14,12 +14,9 @@ class ModuleDocFragment(object):
|
||||
DOCUMENTATION = r'''
|
||||
description:
|
||||
- This module allows one to (re)generate OpenSSL certificates.
|
||||
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL.
|
||||
- If both the cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
|
||||
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with C(select_crypto_backend)).
|
||||
Please note that the PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0.
|
||||
- It uses the cryptography python library to interact with OpenSSL.
|
||||
requirements:
|
||||
- PyOpenSSL >= 0.15 or cryptography >= 1.6 (if using C(selfsigned), C(ownca) or C(assertonly) provider)
|
||||
- cryptography >= 1.6 (if using C(selfsigned) or C(ownca) provider)
|
||||
options:
|
||||
force:
|
||||
description:
|
||||
@@ -55,17 +52,22 @@ options:
|
||||
- This is required if the private key is password protected.
|
||||
type: str
|
||||
|
||||
ignore_timestamps:
|
||||
description:
|
||||
- Whether the "not before" and "not after" timestamps should be ignored for idempotency checks.
|
||||
- It is better to keep the default value C(true) when using relative timestamps (like C(+0s) for now).
|
||||
type: bool
|
||||
default: true
|
||||
version_added: 2.0.0
|
||||
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
choices: [ auto, cryptography ]
|
||||
|
||||
notes:
|
||||
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
|
||||
@@ -119,201 +121,6 @@ options:
|
||||
default: https://acme-v02.api.letsencrypt.org/directory
|
||||
'''
|
||||
|
||||
BACKEND_ASSERTONLY_DOCUMENTATION = r'''
|
||||
description:
|
||||
- The C(assertonly) provider is intended for use cases where one is only interested in
|
||||
checking properties of a supplied certificate. Please note that this provider has been
|
||||
deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0. See the examples on how
|
||||
to emulate C(assertonly) usage with M(community.crypto.x509_certificate_info),
|
||||
M(community.crypto.openssl_csr_info), M(community.crypto.openssl_privatekey_info) and
|
||||
M(ansible.builtin.assert). This also allows more flexible checks than
|
||||
the ones offered by the C(assertonly) provider.
|
||||
- Many properties that can be specified in this module are for validation of an
|
||||
existing or newly generated certificate. The proper place to specify them, if you
|
||||
want to receive a certificate with these properties is a CSR (Certificate Signing Request).
|
||||
options:
|
||||
csr_path:
|
||||
description:
|
||||
- This is not required for the C(assertonly) provider.
|
||||
|
||||
csr_content:
|
||||
description:
|
||||
- This is not required for the C(assertonly) provider.
|
||||
|
||||
signature_algorithms:
|
||||
description:
|
||||
- A list of algorithms that you would accept the certificate to be signed with
|
||||
(e.g. ['sha256WithRSAEncryption', 'sha512WithRSAEncryption']).
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: list
|
||||
elements: str
|
||||
|
||||
issuer:
|
||||
description:
|
||||
- The key/value pairs that must be present in the issuer name field of the certificate.
|
||||
- If you need to specify more than one value with the same key, use a list as value.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: dict
|
||||
|
||||
issuer_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(issuer) field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
subject:
|
||||
description:
|
||||
- The key/value pairs that must be present in the subject name field of the certificate.
|
||||
- If you need to specify more than one value with the same key, use a list as value.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: dict
|
||||
|
||||
subject_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(subject) field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
has_expired:
|
||||
description:
|
||||
- Checks if the certificate is expired/not expired at the time the module is executed.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
version:
|
||||
description:
|
||||
- The version of the certificate.
|
||||
- Nowadays it should almost always be 3.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: int
|
||||
|
||||
valid_at:
|
||||
description:
|
||||
- The certificate must be valid at this point in time.
|
||||
- The timestamp is formatted as an ASN.1 TIME.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
|
||||
invalid_at:
|
||||
description:
|
||||
- The certificate must be invalid at this point in time.
|
||||
- The timestamp is formatted as an ASN.1 TIME.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
|
||||
not_before:
|
||||
description:
|
||||
- The certificate must start to become valid at this point in time.
|
||||
- The timestamp is formatted as an ASN.1 TIME.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
aliases: [ notBefore ]
|
||||
|
||||
not_after:
|
||||
description:
|
||||
- The certificate must expire at this point in time.
|
||||
- The timestamp is formatted as an ASN.1 TIME.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
aliases: [ notAfter ]
|
||||
|
||||
valid_in:
|
||||
description:
|
||||
- The certificate must still be valid at this relative time offset from now.
|
||||
- Valid format is C([+-]timespec | number_of_seconds) where timespec can be an integer
|
||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
||||
- Note that if using this parameter, this module is NOT idempotent.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: str
|
||||
|
||||
key_usage:
|
||||
description:
|
||||
- The I(key_usage) extension field must contain all these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ keyUsage ]
|
||||
|
||||
key_usage_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(key_usage) extension field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
aliases: [ keyUsage_strict ]
|
||||
|
||||
extended_key_usage:
|
||||
description:
|
||||
- The I(extended_key_usage) extension field must contain all these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ extendedKeyUsage ]
|
||||
|
||||
extended_key_usage_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(extended_key_usage) extension field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
aliases: [ extendedKeyUsage_strict ]
|
||||
|
||||
subject_alt_name:
|
||||
description:
|
||||
- The I(subject_alt_name) extension field must contain these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: list
|
||||
elements: str
|
||||
aliases: [ subjectAltName ]
|
||||
|
||||
subject_alt_name_strict:
|
||||
description:
|
||||
- If set to C(yes), the I(subject_alt_name) extension field must contain only these values.
|
||||
- This is only used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
aliases: [ subjectAltName_strict ]
|
||||
'''
|
||||
|
||||
BACKEND_ENTRUST_DOCUMENTATION = r'''
|
||||
options:
|
||||
entrust_cert_type:
|
||||
@@ -386,6 +193,7 @@ options:
|
||||
- The minimum certificate lifetime is 90 days, and maximum is three years.
|
||||
- If this value is not specified, the certificate will stop being valid 365 days the date of issue.
|
||||
- This is only used by the C(entrust) provider.
|
||||
- Please note that this value is B(not) covered by the I(ignore_timestamps) option.
|
||||
type: str
|
||||
default: +365d
|
||||
|
||||
@@ -457,8 +265,10 @@ 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 can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should
|
||||
avoid relative timestamps when setting I(ignore_timestamps=false).
|
||||
- This is only used by the C(ownca) provider.
|
||||
type: str
|
||||
default: +0s
|
||||
@@ -470,8 +280,10 @@ 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 can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should
|
||||
avoid relative timestamps when setting I(ignore_timestamps=false).
|
||||
- This is only used by the C(ownca) provider.
|
||||
- On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer.
|
||||
Please see U(https://support.apple.com/en-us/HT210176) for more details.
|
||||
@@ -548,8 +360,10 @@ 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 can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should
|
||||
avoid relative timestamps when setting I(ignore_timestamps=false).
|
||||
- This is only used by the C(selfsigned) provider.
|
||||
type: str
|
||||
default: +0s
|
||||
@@ -562,8 +376,10 @@ 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 can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should
|
||||
avoid relative timestamps when setting I(ignore_timestamps=false).
|
||||
- This is only used by the C(selfsigned) provider.
|
||||
- On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer.
|
||||
Please see U(https://support.apple.com/en-us/HT210176) for more details.
|
||||
|
||||
@@ -15,13 +15,8 @@ description:
|
||||
- This module allows one to (re)generate OpenSSL certificate signing requests.
|
||||
- This module supports the subjectAltName, keyUsage, extendedKeyUsage, basicConstraints and OCSP Must Staple
|
||||
extensions.
|
||||
- "The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the
|
||||
PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
|
||||
requirements:
|
||||
- Either cryptography >= 1.3
|
||||
- Or pyOpenSSL >= 0.15
|
||||
- cryptography >= 1.3
|
||||
options:
|
||||
digest:
|
||||
description:
|
||||
@@ -48,14 +43,29 @@ options:
|
||||
- The version of the certificate signing request.
|
||||
- "The only allowed value according to L(RFC 2986,https://tools.ietf.org/html/rfc2986#section-4.1)
|
||||
is 1."
|
||||
- This option will no longer accept unsupported values from community.crypto 2.0.0 on.
|
||||
- This option no longer accepts unsupported values since community.crypto 2.0.0.
|
||||
type: int
|
||||
default: 1
|
||||
choices:
|
||||
- 1
|
||||
subject:
|
||||
description:
|
||||
- Key/value pairs that will be present in the subject name field of the certificate signing request.
|
||||
- If you need to specify more than one value with the same key, use a list as value.
|
||||
- If the order of the components is important, use I(subject_ordered).
|
||||
- Mutually exclusive with I(subject_ordered).
|
||||
type: dict
|
||||
subject_ordered:
|
||||
description:
|
||||
- A list of dictionaries, where every dictionary must contain one key/value pair. This key/value pair
|
||||
will be present in the subject name field of the certificate signing request.
|
||||
- If you want to specify more than one value with the same key in a row, you can use a list as value.
|
||||
- Mutually exclusive with I(subject), and any other subject field option, such as I(country_name),
|
||||
I(state_or_province_name), I(locality_name), I(organization_name), I(organizational_unit_name),
|
||||
I(common_name), or I(email_address).
|
||||
type: list
|
||||
elements: dict
|
||||
version_added: 2.0.0
|
||||
country_name:
|
||||
description:
|
||||
- The countryName field of the certificate signing request subject.
|
||||
@@ -196,14 +206,11 @@ options:
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
choices: [ auto, cryptography ]
|
||||
create_subject_key_identifier:
|
||||
description:
|
||||
- Create the Subject Key Identifier from the public key.
|
||||
@@ -227,12 +234,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 +247,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:
|
||||
|
||||
@@ -22,13 +22,8 @@ description:
|
||||
(or specify none), change the keysize, etc., the private key will be
|
||||
regenerated. If you are concerned that this could B(overwrite your private key),
|
||||
consider using the I(backup) option."
|
||||
- "The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the
|
||||
PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
|
||||
requirements:
|
||||
- Either cryptography >= 1.2.3 (older versions might work as well)
|
||||
- Or pyOpenSSL
|
||||
- cryptography >= 1.2.3 (older versions might work as well)
|
||||
options:
|
||||
size:
|
||||
description:
|
||||
@@ -80,22 +75,16 @@ options:
|
||||
type: str
|
||||
cipher:
|
||||
description:
|
||||
- The cipher to encrypt the private key. (Valid values can be found by
|
||||
running `openssl list -cipher-algorithms` or `openssl list-cipher-algorithms`,
|
||||
depending on your OpenSSL version.)
|
||||
- When using the C(cryptography) backend, use C(auto).
|
||||
- The cipher to encrypt the private key. Must be C(auto).
|
||||
type: str
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
choices: [ auto, cryptography ]
|
||||
format:
|
||||
description:
|
||||
- Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format)
|
||||
@@ -105,8 +94,6 @@ options:
|
||||
selected one for generation.
|
||||
- Note that if the format for an existing private key mismatches, the key is B(regenerated) by default.
|
||||
To change this behavior, use the I(format_mismatch) option.
|
||||
- The I(format) option is only supported by the C(cryptography) backend. The C(pyopenssl) backend will
|
||||
fail if a value different from C(auto_ignore) is used.
|
||||
type: str
|
||||
default: auto_ignore
|
||||
choices: [ pkcs1, pkcs8, raw, auto, auto_ignore ]
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import copy
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
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_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||
get_default_argspec,
|
||||
ACMEDirectory,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import (
|
||||
CryptographyBackend,
|
||||
CRYPTOGRAPHY_VERSION,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
|
||||
OpenSSLCLIBackend,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme._compatibility import (
|
||||
handle_standard_module_arguments,
|
||||
set_crypto_backend,
|
||||
HAS_CURRENT_CRYPTOGRAPHY,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme._compatibility import ACMELegacyAccount as ACMEAccount
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.io import (
|
||||
read_file,
|
||||
write_file,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
|
||||
nopad_b64,
|
||||
pem_to_der,
|
||||
process_links,
|
||||
)
|
||||
|
||||
|
||||
def openssl_get_csr_identifiers(openssl_binary, module, csr_filename, csr_content=None):
|
||||
module.deprecate(
|
||||
'Please adjust your custom module/plugin to the ACME module_utils refactor '
|
||||
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
|
||||
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
|
||||
'your code', version='2.0.0', collection_name='community.crypto')
|
||||
return OpenSSLCLIBackend(module, openssl_binary=openssl_binary).get_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content)
|
||||
|
||||
|
||||
def cryptography_get_csr_identifiers(module, csr_filename, csr_content=None):
|
||||
module.deprecate(
|
||||
'Please adjust your custom module/plugin to the ACME module_utils refactor '
|
||||
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
|
||||
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
|
||||
'your code', version='2.0.0', collection_name='community.crypto')
|
||||
return CryptographyBackend(module).get_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content)
|
||||
|
||||
|
||||
def cryptography_get_cert_days(module, cert_file, now=None):
|
||||
module.deprecate(
|
||||
'Please adjust your custom module/plugin to the ACME module_utils refactor '
|
||||
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
|
||||
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
|
||||
'your code', version='2.0.0', collection_name='community.crypto')
|
||||
return CryptographyBackend(module).get_cert_days(cert_filename=cert_file, now=now)
|
||||
@@ -1,267 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import locale
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import HAS_CURRENT_CRYPTOGRAPHY as _ORIGINAL_HAS_CURRENT_CRYPTOGRAPHY
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import (
|
||||
CryptographyBackend,
|
||||
CRYPTOGRAPHY_VERSION,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
|
||||
OpenSSLCLIBackend,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||
ACMEClient,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
|
||||
ACMEAccount,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import (
|
||||
create_key_authorization,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
|
||||
KeyParsingError,
|
||||
)
|
||||
|
||||
|
||||
HAS_CURRENT_CRYPTOGRAPHY = _ORIGINAL_HAS_CURRENT_CRYPTOGRAPHY
|
||||
|
||||
|
||||
def set_crypto_backend(module):
|
||||
'''
|
||||
Sets which crypto backend to use (default: auto detection).
|
||||
|
||||
Does not care whether a new enough cryptoraphy is available or not. Must
|
||||
be called before any real stuff is done which might evaluate
|
||||
``HAS_CURRENT_CRYPTOGRAPHY``.
|
||||
'''
|
||||
global HAS_CURRENT_CRYPTOGRAPHY
|
||||
|
||||
module.deprecate(
|
||||
'Please adjust your custom module/plugin to the ACME module_utils refactor '
|
||||
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
|
||||
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
|
||||
'your code', version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
# Choose backend
|
||||
backend = module.params['select_crypto_backend']
|
||||
if backend == 'auto':
|
||||
pass
|
||||
elif backend == 'openssl':
|
||||
HAS_CURRENT_CRYPTOGRAPHY = False
|
||||
elif backend == 'cryptography':
|
||||
if not _ORIGINAL_HAS_CURRENT_CRYPTOGRAPHY:
|
||||
module.fail_json(msg=missing_required_lib('cryptography'))
|
||||
HAS_CURRENT_CRYPTOGRAPHY = True
|
||||
else:
|
||||
module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend))
|
||||
# Inform about choices
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
module.debug('Using cryptography backend (library version {0})'.format(CRYPTOGRAPHY_VERSION))
|
||||
return 'cryptography'
|
||||
else:
|
||||
module.debug('Using OpenSSL binary backend')
|
||||
return 'openssl'
|
||||
|
||||
|
||||
def handle_standard_module_arguments(module, needs_acme_v2=False):
|
||||
'''
|
||||
Do standard module setup, argument handling and warning emitting.
|
||||
'''
|
||||
backend = set_crypto_backend(module)
|
||||
|
||||
if not module.params['validate_certs']:
|
||||
module.warn(
|
||||
'Disabling certificate validation for communications with ACME endpoint. '
|
||||
'This should only be done for testing against a local ACME server for '
|
||||
'development purposes, but *never* for production purposes.'
|
||||
)
|
||||
|
||||
if module.params['acme_version'] is None:
|
||||
module.params['acme_version'] = 1
|
||||
module.deprecate("The option 'acme_version' will be required from community.crypto 2.0.0 on",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if module.params['acme_directory'] is None:
|
||||
module.params['acme_directory'] = 'https://acme-staging.api.letsencrypt.org/directory'
|
||||
module.deprecate("The option 'acme_directory' will be required from community.crypto 2.0.0 on",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if needs_acme_v2 and module.params['acme_version'] < 2:
|
||||
module.fail_json(msg='The {0} module requires the ACME v2 protocol!'.format(module._name))
|
||||
|
||||
# AnsibleModule() changes the locale, so change it back to C because we rely on time.strptime() when parsing certificate dates.
|
||||
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
||||
locale.setlocale(locale.LC_ALL, 'C')
|
||||
|
||||
return backend
|
||||
|
||||
|
||||
def get_compatibility_backend(module):
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
return CryptographyBackend(module)
|
||||
else:
|
||||
return OpenSSLCLIBackend(module)
|
||||
|
||||
|
||||
class ACMELegacyAccount(object):
|
||||
'''
|
||||
ACME account object. Handles the authorized communication with the
|
||||
ACME server. Provides access to account bound information like
|
||||
the currently active authorizations and valid certificates
|
||||
'''
|
||||
|
||||
def __init__(self, module):
|
||||
module.deprecate(
|
||||
'Please adjust your custom module/plugin to the ACME module_utils refactor '
|
||||
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
|
||||
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
|
||||
'your code', version='2.0.0', collection_name='community.crypto')
|
||||
backend = get_compatibility_backend(module)
|
||||
self.client = ACMEClient(module, backend)
|
||||
self.account = ACMEAccount(self.client)
|
||||
self.key = self.client.account_key_file
|
||||
self.key_content = self.client.account_key_content
|
||||
self.uri = self.client.account_uri
|
||||
self.key_data = self.client.account_key_data
|
||||
self.jwk = self.client.account_jwk
|
||||
self.jws_header = self.client.account_jws_header
|
||||
self.directory = self.client.directory
|
||||
|
||||
def get_keyauthorization(self, token):
|
||||
'''
|
||||
Returns the key authorization for the given token
|
||||
https://tools.ietf.org/html/rfc8555#section-8.1
|
||||
'''
|
||||
return create_key_authorization(self.client, token)
|
||||
|
||||
def parse_key(self, key_file=None, key_content=None):
|
||||
'''
|
||||
Parses an RSA or Elliptic Curve key file in PEM format and returns a pair
|
||||
(error, key_data).
|
||||
'''
|
||||
try:
|
||||
return None, self.client.parse_key(key_file=key_file, key_content=key_content)
|
||||
except KeyParsingError as e:
|
||||
return e.msg, {}
|
||||
|
||||
def sign_request(self, protected, payload, key_data, encode_payload=True):
|
||||
return self.client.sign_request(protected, payload, key_data, encode_payload=encode_payload)
|
||||
|
||||
def send_signed_request(self, url, payload, key_data=None, jws_header=None, parse_json_result=True, encode_payload=True):
|
||||
'''
|
||||
Sends a JWS signed HTTP POST request to the ACME server and returns
|
||||
the response as dictionary
|
||||
https://tools.ietf.org/html/rfc8555#section-6.2
|
||||
|
||||
If payload is None, a POST-as-GET is performed.
|
||||
(https://tools.ietf.org/html/rfc8555#section-6.3)
|
||||
'''
|
||||
return self.client.send_signed_request(
|
||||
url,
|
||||
payload,
|
||||
key_data=key_data,
|
||||
jws_header=jws_header,
|
||||
parse_json_result=parse_json_result,
|
||||
encode_payload=encode_payload,
|
||||
fail_on_error=False,
|
||||
)
|
||||
|
||||
def get_request(self, uri, parse_json_result=True, headers=None, get_only=False, fail_on_error=True):
|
||||
'''
|
||||
Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback
|
||||
to GET if server replies with a status code of 405.
|
||||
'''
|
||||
return self.client.get_request(
|
||||
uri,
|
||||
parse_json_result=parse_json_result,
|
||||
headers=headers,
|
||||
get_only=get_only,
|
||||
fail_on_error=fail_on_error,
|
||||
)
|
||||
|
||||
def set_account_uri(self, uri):
|
||||
'''
|
||||
Set account URI. For ACME v2, it needs to be used to sending signed
|
||||
requests.
|
||||
'''
|
||||
self.client.set_account_uri(uri)
|
||||
self.uri = self.client.account_uri
|
||||
|
||||
def get_account_data(self):
|
||||
'''
|
||||
Retrieve account information. Can only be called when the account
|
||||
URI is already known (such as after calling setup_account).
|
||||
Return None if the account was deactivated, or a dict otherwise.
|
||||
'''
|
||||
return self.account.get_account_data()
|
||||
|
||||
def setup_account(self, contact=None, agreement=None, terms_agreed=False,
|
||||
allow_creation=True, remove_account_uri_if_not_exists=False,
|
||||
external_account_binding=None):
|
||||
'''
|
||||
Detect or create an account on the ACME server. For ACME v1,
|
||||
as the only way (without knowing an account URI) to test if an
|
||||
account exists is to try and create one with the provided account
|
||||
key, this method will always result in an account being present
|
||||
(except on error situations). For ACME v2, a new account will
|
||||
only be created if ``allow_creation`` is set to True.
|
||||
|
||||
For ACME v2, ``check_mode`` is fully respected. For ACME v1, the
|
||||
account might be created if it does not yet exist.
|
||||
|
||||
Return a pair ``(created, account_data)``. Here, ``created`` will
|
||||
be ``True`` in case the account was created or would be created
|
||||
(check mode). ``account_data`` will be the current account data,
|
||||
or ``None`` if the account does not exist.
|
||||
|
||||
The account URI will be stored in ``self.uri``; if it is ``None``,
|
||||
the account does not exist.
|
||||
|
||||
If specified, ``external_account_binding`` should be a dictionary
|
||||
with keys ``kid``, ``alg`` and ``key``
|
||||
(https://tools.ietf.org/html/rfc8555#section-7.3.4).
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.3
|
||||
'''
|
||||
result = self.account.setup_account(
|
||||
contact=contact,
|
||||
agreement=agreement,
|
||||
terms_agreed=terms_agreed,
|
||||
allow_creation=allow_creation,
|
||||
remove_account_uri_if_not_exists=remove_account_uri_if_not_exists,
|
||||
external_account_binding=external_account_binding,
|
||||
)
|
||||
self.uri = self.client.account_uri
|
||||
return result
|
||||
|
||||
def update_account(self, account_data, contact=None):
|
||||
'''
|
||||
Update an account on the ACME server. Check mode is fully respected.
|
||||
|
||||
The current account data must be provided as ``account_data``.
|
||||
|
||||
Return a pair ``(updated, account_data)``, where ``updated`` is
|
||||
``True`` in case something changed (contact info updated) or
|
||||
would be changed (check mode), and ``account_data`` the updated
|
||||
account data.
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.3.2
|
||||
'''
|
||||
return self.account.update_account(account_data, contact=contact)
|
||||
@@ -12,10 +12,12 @@ import copy
|
||||
import datetime
|
||||
import json
|
||||
import locale
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils._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,
|
||||
@@ -38,6 +40,14 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.utils import
|
||||
nopad_b64,
|
||||
)
|
||||
|
||||
try:
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
HAS_IPADDRESS = False
|
||||
IPADDRESS_IMPORT_ERROR = traceback.format_exc()
|
||||
else:
|
||||
HAS_IPADDRESS = True
|
||||
|
||||
|
||||
def _assert_fetch_url_success(module, response, info, allow_redirect=False, allow_client_error=True, allow_server_error=True):
|
||||
if info['status'] < 0:
|
||||
@@ -228,9 +238,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 +299,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
|
||||
@@ -319,14 +338,17 @@ def get_default_argspec():
|
||||
account_key_content=dict(type='str', no_log=True),
|
||||
account_key_passphrase=dict(type='str', no_log=True),
|
||||
account_uri=dict(type='str'),
|
||||
acme_directory=dict(type='str'),
|
||||
acme_version=dict(type='int', choices=[1, 2]),
|
||||
acme_directory=dict(type='str', required=True),
|
||||
acme_version=dict(type='int', required=True, choices=[1, 2]),
|
||||
validate_certs=dict(type='bool', default=True),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']),
|
||||
)
|
||||
|
||||
|
||||
def create_backend(module, needs_acme_v2):
|
||||
if not HAS_IPADDRESS:
|
||||
module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMPORT_ERROR)
|
||||
|
||||
backend = module.params['select_crypto_backend']
|
||||
|
||||
# Backend autodetect
|
||||
@@ -353,19 +375,13 @@ def create_backend(module, needs_acme_v2):
|
||||
'development purposes, but *never* for production purposes.'
|
||||
)
|
||||
|
||||
if module.params['acme_version'] is None:
|
||||
module.params['acme_version'] = 1
|
||||
module.deprecate("The option 'acme_version' will be required from community.crypto 2.0.0 on",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if module.params['acme_directory'] is None:
|
||||
module.params['acme_directory'] = 'https://acme-staging.api.letsencrypt.org/directory'
|
||||
module.deprecate("The option 'acme_directory' will be required from community.crypto 2.0.0 on",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if needs_acme_v2 and module.params['acme_version'] < 2:
|
||||
module.fail_json(msg='The {0} module requires the ACME v2 protocol!'.format(module._name))
|
||||
|
||||
if module.params['acme_version'] == 1:
|
||||
module.deprecate("The value 1 for 'acme_version' is deprecated. Please switch to ACME v2",
|
||||
version='3.0.0', collection_name='community.crypto')
|
||||
|
||||
# AnsibleModule() changes the locale, so change it back to C because we rely
|
||||
# on datetime.datetime.strptime() when parsing certificate dates.
|
||||
locale.setlocale(locale.LC_ALL, 'C')
|
||||
|
||||
@@ -14,7 +14,7 @@ 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.acme.backends import (
|
||||
CryptoBackend,
|
||||
@@ -41,6 +41,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
|
||||
@@ -118,11 +122,11 @@ class CryptographyChainMatcher(ChainMatcher):
|
||||
self.issuer = []
|
||||
if criterium.subject:
|
||||
self.subject = [
|
||||
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.subject)
|
||||
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.subject, 'subject')
|
||||
]
|
||||
if criterium.issuer:
|
||||
self.issuer = [
|
||||
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.issuer)
|
||||
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.issuer, 'issuer')
|
||||
]
|
||||
self.subject_key_identifier = CryptographyChainMatcher._parse_key_identifier(
|
||||
criterium.subject_key_identifier, 'subject_key_identifier', criterium.index, module)
|
||||
@@ -357,6 +361,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,
|
||||
@@ -29,7 +29,10 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
||||
try:
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
_OPENSSL_ENVIRONMENT_UPDATE = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
||||
@@ -216,7 +219,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
@staticmethod
|
||||
def _normalize_ip(ip):
|
||||
try:
|
||||
return to_native(compat_ipaddress.ip_address(to_text(ip)).compressed)
|
||||
return to_native(ipaddress.ip_address(to_text(ip)).compressed)
|
||||
except ValueError:
|
||||
# We don't want to error out on something IPAddress() can't parse
|
||||
return ip
|
||||
@@ -230,7 +233,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 +270,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,9 +14,7 @@ import json
|
||||
import re
|
||||
import time
|
||||
|
||||
from ansible.module_utils._text import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
|
||||
nopad_b64,
|
||||
@@ -28,6 +26,11 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
|
||||
ModuleFailException,
|
||||
)
|
||||
|
||||
try:
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def create_key_authorization(client, token):
|
||||
'''
|
||||
@@ -110,7 +113,7 @@ class Challenge(object):
|
||||
# https://www.rfc-editor.org/rfc/rfc8737.html#section-3
|
||||
if identifier_type == 'ip':
|
||||
# IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596)
|
||||
resource = compat_ipaddress.ip_address(identifier).reverse_pointer
|
||||
resource = ipaddress.ip_address(identifier).reverse_pointer
|
||||
if not resource.endswith('.'):
|
||||
resource += '.'
|
||||
else:
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,99 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
#
|
||||
# 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
|
||||
|
||||
|
||||
# THIS FILE IS FOR COMPATIBILITY ONLY! YOU SHALL NOT IMPORT IT!
|
||||
#
|
||||
# This fill will be removed eventually, so if you're using it,
|
||||
# please stop doing so.
|
||||
|
||||
from .basic import (
|
||||
HAS_PYOPENSSL,
|
||||
CRYPTOGRAPHY_HAS_X25519,
|
||||
CRYPTOGRAPHY_HAS_X25519_FULL,
|
||||
CRYPTOGRAPHY_HAS_X448,
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
HAS_CRYPTOGRAPHY,
|
||||
OpenSSLObjectError,
|
||||
OpenSSLBadPassphraseError,
|
||||
)
|
||||
|
||||
from .cryptography_crl import (
|
||||
REVOCATION_REASON_MAP,
|
||||
REVOCATION_REASON_MAP_INVERSE,
|
||||
cryptography_decode_revoked_certificate,
|
||||
)
|
||||
|
||||
from .cryptography_support import (
|
||||
cryptography_get_extensions_from_cert,
|
||||
cryptography_get_extensions_from_csr,
|
||||
cryptography_name_to_oid,
|
||||
cryptography_oid_to_name,
|
||||
cryptography_get_name,
|
||||
cryptography_decode_name,
|
||||
cryptography_parse_key_usage_params,
|
||||
cryptography_get_basic_constraints,
|
||||
cryptography_key_needs_digest_for_signing,
|
||||
cryptography_compare_public_keys,
|
||||
)
|
||||
|
||||
from .pem import (
|
||||
identify_private_key_format,
|
||||
)
|
||||
|
||||
from .math import (
|
||||
binary_exp_mod,
|
||||
simple_gcd,
|
||||
quick_is_not_prime,
|
||||
count_bits,
|
||||
)
|
||||
|
||||
from ._obj2txt import obj2txt as _obj2txt
|
||||
|
||||
from ._objects_data import OID_MAP as _OID_MAP
|
||||
|
||||
from ._objects import OID_LOOKUP as _OID_LOOKUP
|
||||
from ._objects import NORMALIZE_NAMES as _NORMALIZE_NAMES
|
||||
from ._objects import NORMALIZE_NAMES_SHORT as _NORMALIZE_NAMES_SHORT
|
||||
|
||||
from .pyopenssl_support import (
|
||||
pyopenssl_normalize_name,
|
||||
pyopenssl_get_extensions_from_cert,
|
||||
pyopenssl_get_extensions_from_csr,
|
||||
)
|
||||
|
||||
from .support import (
|
||||
get_fingerprint_of_bytes,
|
||||
get_fingerprint,
|
||||
load_privatekey,
|
||||
load_certificate,
|
||||
load_certificate_request,
|
||||
parse_name_field,
|
||||
convert_relative_to_datetime,
|
||||
get_relative_time_option,
|
||||
select_message_digest,
|
||||
OpenSSLObject,
|
||||
)
|
||||
|
||||
from ..io import (
|
||||
load_file_if_exists,
|
||||
write_file,
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -22,14 +22,6 @@ __metaclass__ = type
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
try:
|
||||
import OpenSSL # noqa
|
||||
from OpenSSL import crypto # noqa
|
||||
HAS_PYOPENSSL = True
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
HAS_PYOPENSSL = False
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
|
||||
@@ -22,13 +22,17 @@ __metaclass__ = type
|
||||
import base64
|
||||
import binascii
|
||||
import re
|
||||
import sys
|
||||
|
||||
from ansible.module_utils._text import to_text, to_bytes
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_text, to_bytes
|
||||
from ._asn1 import serialize_asn1_string_as_der
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
@@ -44,6 +48,15 @@ except ImportError:
|
||||
# Error handled in the calling module.
|
||||
_load_key_and_certificates = None
|
||||
|
||||
try:
|
||||
# This is a separate try/except since this is only present in cryptography 36.0.0 or newer
|
||||
from cryptography.hazmat.primitives.serialization.pkcs12 import (
|
||||
load_pkcs12 as _load_pkcs12,
|
||||
)
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
_load_pkcs12 = None
|
||||
|
||||
from .basic import (
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
@@ -64,60 +77,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
|
||||
|
||||
|
||||
@@ -161,34 +228,68 @@ def _parse_hex(bytesstr):
|
||||
return data
|
||||
|
||||
|
||||
DN_COMPONENT_START_RE = re.compile(r'^ *([a-zA-z0-9]+) *= *')
|
||||
DN_COMPONENT_START_RE = re.compile(b'^ *([a-zA-z0-9.]+) *= *')
|
||||
DN_HEX_LETTER = b'0123456789abcdef'
|
||||
|
||||
|
||||
def _parse_dn_component(name, sep=',', sep_str='\\', decode_remainder=True):
|
||||
if sys.version_info[0] < 3:
|
||||
_int_to_byte = chr
|
||||
else:
|
||||
def _int_to_byte(value):
|
||||
return bytes((value, ))
|
||||
|
||||
|
||||
def _parse_dn_component(name, sep=b',', decode_remainder=True):
|
||||
m = DN_COMPONENT_START_RE.match(name)
|
||||
if not m:
|
||||
raise OpenSSLObjectError('cannot start part in "{0}"'.format(name))
|
||||
oid = cryptography_name_to_oid(m.group(1))
|
||||
raise OpenSSLObjectError(u'cannot start part in "{0}"'.format(to_text(name)))
|
||||
oid = cryptography_name_to_oid(to_text(m.group(1)))
|
||||
idx = len(m.group(0))
|
||||
decoded_name = []
|
||||
sep_str = sep + b'\\'
|
||||
if decode_remainder:
|
||||
length = len(name)
|
||||
while idx < length:
|
||||
i = idx
|
||||
while i < length and name[i] not in sep_str:
|
||||
i += 1
|
||||
if i > idx:
|
||||
decoded_name.append(name[idx:i])
|
||||
idx = i
|
||||
while idx + 1 < length and name[idx] == '\\':
|
||||
decoded_name.append(name[idx + 1])
|
||||
if length > idx and name[idx:idx + 1] == b'#':
|
||||
# Decoding a hex string
|
||||
idx += 1
|
||||
while idx + 1 < length:
|
||||
ch1 = name[idx:idx + 1]
|
||||
ch2 = name[idx + 1:idx + 2]
|
||||
idx1 = DN_HEX_LETTER.find(ch1.lower())
|
||||
idx2 = DN_HEX_LETTER.find(ch2.lower())
|
||||
if idx1 < 0 or idx2 < 0:
|
||||
raise OpenSSLObjectError(u'Invalid hex sequence entry "{0}"'.format(to_text(ch1 + ch2)))
|
||||
idx += 2
|
||||
if idx < length and name[idx] == sep:
|
||||
break
|
||||
decoded_name.append(_int_to_byte(idx1 * 16 + idx2))
|
||||
else:
|
||||
# Decoding a regular string
|
||||
while idx < length:
|
||||
i = idx
|
||||
while i < length and name[i:i + 1] not in sep_str:
|
||||
i += 1
|
||||
if i > idx:
|
||||
decoded_name.append(name[idx:i])
|
||||
idx = i
|
||||
while idx + 1 < length and name[idx:idx + 1] == b'\\':
|
||||
ch = name[idx + 1:idx + 2]
|
||||
idx1 = DN_HEX_LETTER.find(ch.lower())
|
||||
if idx1 >= 0:
|
||||
if idx + 2 >= length:
|
||||
raise OpenSSLObjectError(u'Hex escape sequence "\\{0}" incomplete at end of string'.format(to_text(ch)))
|
||||
ch2 = name[idx + 2:idx + 3]
|
||||
idx2 = DN_HEX_LETTER.find(ch2.lower())
|
||||
if idx2 < 0:
|
||||
raise OpenSSLObjectError(u'Hex escape sequence "\\{0}" has invalid second letter'.format(to_text(ch + ch2)))
|
||||
ch = _int_to_byte(idx1 * 16 + idx2)
|
||||
idx += 1
|
||||
idx += 2
|
||||
decoded_name.append(ch)
|
||||
if idx < length and name[idx:idx + 1] == sep:
|
||||
break
|
||||
else:
|
||||
decoded_name.append(name[idx:])
|
||||
idx = len(name)
|
||||
return x509.NameAttribute(oid, ''.join(decoded_name)), name[idx:]
|
||||
return x509.NameAttribute(oid, to_text(b''.join(decoded_name))), name[idx:]
|
||||
|
||||
|
||||
def _parse_dn(name):
|
||||
@@ -199,21 +300,20 @@ def _parse_dn(name):
|
||||
'''
|
||||
original_name = name
|
||||
name = name.lstrip()
|
||||
sep = ','
|
||||
if name.startswith('/'):
|
||||
sep = '/'
|
||||
sep = b','
|
||||
if name.startswith(b'/'):
|
||||
sep = b'/'
|
||||
name = name[1:]
|
||||
sep_str = sep + '\\'
|
||||
result = []
|
||||
while name:
|
||||
try:
|
||||
attribute, name = _parse_dn_component(name, sep=sep, sep_str=sep_str)
|
||||
attribute, name = _parse_dn_component(name, sep=sep)
|
||||
except OpenSSLObjectError as e:
|
||||
raise OpenSSLObjectError('Error while parsing distinguished name "{0}": {1}'.format(original_name, e))
|
||||
raise OpenSSLObjectError(u'Error while parsing distinguished name "{0}": {1}'.format(to_text(original_name), e))
|
||||
result.append(attribute)
|
||||
if name:
|
||||
if name[0] != sep or len(name) < 2:
|
||||
raise OpenSSLObjectError('Error while parsing distinguished name "{0}": unexpected end of string'.format(original_name))
|
||||
if name[0:1] != sep or len(name) < 2:
|
||||
raise OpenSSLObjectError(u'Error while parsing distinguished name "{0}": unexpected end of string'.format(to_text(original_name)))
|
||||
name = name[1:]
|
||||
return result
|
||||
|
||||
@@ -222,9 +322,9 @@ def cryptography_parse_relative_distinguished_name(rdn):
|
||||
names = []
|
||||
for part in rdn:
|
||||
try:
|
||||
names.append(_parse_dn_component(to_text(part), decode_remainder=False)[0])
|
||||
names.append(_parse_dn_component(to_bytes(part), decode_remainder=False)[0])
|
||||
except OpenSSLObjectError as e:
|
||||
raise OpenSSLObjectError('Error while parsing relative distinguished name "{0}": {1}'.format(part, e))
|
||||
raise OpenSSLObjectError(u'Error while parsing relative distinguished name "{0}": {1}'.format(part, e))
|
||||
return cryptography.x509.RelativeDistinguishedName(names)
|
||||
|
||||
|
||||
@@ -268,7 +368,7 @@ def cryptography_get_name(name, what='Subject Alternative Name'):
|
||||
b_value = serialize_asn1_string_as_der(value)
|
||||
return x509.OtherName(x509.ObjectIdentifier(oid), b_value)
|
||||
if name.startswith('dirName:'):
|
||||
return x509.DirectoryName(x509.Name(_parse_dn(to_text(name[8:]))))
|
||||
return x509.DirectoryName(x509.Name(reversed(_parse_dn(to_bytes(name[8:])))))
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError('Cannot parse {what} "{name}": {error}'.format(name=name, what=what, error=e))
|
||||
if ':' not in name:
|
||||
@@ -280,11 +380,14 @@ 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'"']:
|
||||
value = value.replace(ch, u'\\%s' % ch)
|
||||
value = value.replace(u'\0', u'\\00')
|
||||
if value.startswith((u' ', u'#')):
|
||||
value = u'\\%s' % value[0] + value[1:]
|
||||
if value.endswith(u' '):
|
||||
value = value[:-1] + u'\\ '
|
||||
return value
|
||||
|
||||
|
||||
@@ -294,24 +397,26 @@ 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))
|
||||
for attribute in name.value
|
||||
# According to https://datatracker.ietf.org/doc/html/rfc4514.html#section-2.1 the
|
||||
# list needs to be reversed, and joined by commas
|
||||
return u'dirName:' + ','.join([
|
||||
u'{0}={1}'.format(to_text(cryptography_oid_to_name(attribute.oid, short=True)), _dn_escape_value(attribute.value))
|
||||
for attribute in reversed(list(name.value))
|
||||
])
|
||||
if isinstance(name, x509.RegisteredID):
|
||||
return 'RID:{0}'.format(name.value.dotted_string)
|
||||
return u'RID:{0}'.format(name.value.dotted_string)
|
||||
if isinstance(name, x509.OtherName):
|
||||
return 'otherName:{0};{1}'.format(name.type_id.dotted_string, _get_hex(name.value))
|
||||
return u'otherName:{0};{1}'.format(name.type_id.dotted_string, _get_hex(name.value))
|
||||
raise OpenSSLObjectError('Cannot decode name "{0}"'.format(name))
|
||||
|
||||
|
||||
@@ -442,17 +547,74 @@ def cryptography_serial_number_of_cert(cert):
|
||||
def parse_pkcs12(pkcs12_bytes, passphrase=None):
|
||||
'''Returns a tuple (private_key, certificate, additional_certificates, friendly_name).
|
||||
'''
|
||||
if _load_key_and_certificates is None:
|
||||
raise ValueError('load_key_and_certificates() not present in the current cryptography version')
|
||||
private_key, certificate, additional_certificates = _load_key_and_certificates(
|
||||
pkcs12_bytes, to_bytes(passphrase) if passphrase is not None else None)
|
||||
if _load_pkcs12 is None and _load_key_and_certificates is None:
|
||||
raise ValueError('neither load_pkcs12() nor load_key_and_certificates() present in the current cryptography version')
|
||||
|
||||
if passphrase is not None:
|
||||
passphrase = to_bytes(passphrase)
|
||||
|
||||
# Main code for cryptography 36.0.0 and forward
|
||||
if _load_pkcs12 is not None:
|
||||
return _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase)
|
||||
|
||||
if LooseVersion(cryptography.__version__) >= LooseVersion('35.0'):
|
||||
return _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase)
|
||||
|
||||
return _parse_pkcs12_legacy(pkcs12_bytes, passphrase)
|
||||
|
||||
|
||||
def _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase=None):
|
||||
# Requires cryptography 36.0.0 or newer
|
||||
pkcs12 = _load_pkcs12(pkcs12_bytes, passphrase)
|
||||
additional_certificates = [cert.certificate for cert in pkcs12.additional_certs]
|
||||
private_key = pkcs12.key
|
||||
certificate = None
|
||||
friendly_name = None
|
||||
if pkcs12.cert:
|
||||
certificate = pkcs12.cert.certificate
|
||||
friendly_name = pkcs12.cert.friendly_name
|
||||
return private_key, certificate, additional_certificates, friendly_name
|
||||
|
||||
|
||||
def _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase=None):
|
||||
# Backwards compatibility code for cryptography 35.x
|
||||
private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase)
|
||||
|
||||
friendly_name = None
|
||||
if certificate:
|
||||
# See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238
|
||||
maybe_name = certificate._backend._lib.X509_alias_get0(
|
||||
certificate._x509, certificate._backend._ffi.NULL)
|
||||
if maybe_name != certificate._backend._ffi.NULL:
|
||||
friendly_name = certificate._backend._ffi.string(maybe_name)
|
||||
backend = default_backend()
|
||||
|
||||
# This code basically does what load_key_and_certificates() does, but without error-checking.
|
||||
# Since load_key_and_certificates succeeded, it should not fail.
|
||||
pkcs12 = backend._ffi.gc(
|
||||
backend._lib.d2i_PKCS12_bio(backend._bytes_to_bio(pkcs12_bytes).bio, backend._ffi.NULL),
|
||||
backend._lib.PKCS12_free)
|
||||
certificate_x509_ptr = backend._ffi.new("X509 **")
|
||||
with backend._zeroed_null_terminated_buf(to_bytes(passphrase) if passphrase is not None else None) as passphrase_buffer:
|
||||
backend._lib.PKCS12_parse(
|
||||
pkcs12,
|
||||
passphrase_buffer,
|
||||
backend._ffi.new("EVP_PKEY **"),
|
||||
certificate_x509_ptr,
|
||||
backend._ffi.new("Cryptography_STACK_OF_X509 **"))
|
||||
if certificate_x509_ptr[0] != backend._ffi.NULL:
|
||||
maybe_name = backend._lib.X509_alias_get0(certificate_x509_ptr[0], backend._ffi.NULL)
|
||||
if maybe_name != backend._ffi.NULL:
|
||||
friendly_name = backend._ffi.string(maybe_name)
|
||||
|
||||
return private_key, certificate, additional_certificates, friendly_name
|
||||
|
||||
|
||||
def _parse_pkcs12_legacy(pkcs12_bytes, passphrase=None):
|
||||
# Backwards compatibility code for cryptography < 35.0.0
|
||||
private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase)
|
||||
|
||||
friendly_name = None
|
||||
if certificate:
|
||||
# See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238
|
||||
backend = certificate._backend
|
||||
maybe_name = backend._lib.X509_alias_get0(certificate._x509, backend._ffi.NULL)
|
||||
if maybe_name != backend._ffi.NULL:
|
||||
friendly_name = backend._ffi.string(maybe_name)
|
||||
return private_key, certificate, additional_certificates, friendly_name
|
||||
|
||||
@@ -38,18 +38,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
|
||||
)
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
CRYPTOGRAPHY_VERSION = None
|
||||
@@ -75,6 +63,7 @@ class CertificateBackend(object):
|
||||
self.backend = backend
|
||||
|
||||
self.force = module.params['force']
|
||||
self.ignore_timestamps = module.params['ignore_timestamps']
|
||||
self.privatekey_path = module.params['privatekey_path']
|
||||
self.privatekey_content = module.params['privatekey_content']
|
||||
if self.privatekey_content is not None:
|
||||
@@ -173,43 +162,12 @@ class CertificateBackend(object):
|
||||
|
||||
def _check_privatekey(self):
|
||||
"""Check whether provided parameters match, assuming self.existing_certificate and self.privatekey have been populated."""
|
||||
if self.backend == 'pyopenssl':
|
||||
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD)
|
||||
ctx.use_privatekey(self.privatekey)
|
||||
ctx.use_certificate(self.existing_certificate)
|
||||
try:
|
||||
ctx.check_privatekey()
|
||||
return True
|
||||
except OpenSSL.SSL.Error:
|
||||
return False
|
||||
elif self.backend == 'cryptography':
|
||||
if self.backend == 'cryptography':
|
||||
return cryptography_compare_public_keys(self.existing_certificate.public_key(), self.privatekey.public_key())
|
||||
|
||||
def _check_csr(self):
|
||||
"""Check whether provided parameters match, assuming self.existing_certificate and self.csr have been populated."""
|
||||
if self.backend == 'pyopenssl':
|
||||
# Verify that CSR is signed by certificate's private key
|
||||
try:
|
||||
self.csr.verify(self.existing_certificate.get_pubkey())
|
||||
except OpenSSL.crypto.Error:
|
||||
return False
|
||||
# Check subject
|
||||
if self.check_csr_subject and self.csr.get_subject() != self.existing_certificate.get_subject():
|
||||
return False
|
||||
# Check extensions
|
||||
if not self.check_csr_extensions:
|
||||
return True
|
||||
csr_extensions = self.csr.get_extensions()
|
||||
cert_extension_count = self.existing_certificate.get_extension_count()
|
||||
if len(csr_extensions) != cert_extension_count:
|
||||
return False
|
||||
for extension_number in range(0, cert_extension_count):
|
||||
cert_extension = self.existing_certificate.get_extension(extension_number)
|
||||
csr_extension = filter(lambda extension: extension.get_short_name() == cert_extension.get_short_name(), csr_extensions)
|
||||
if cert_extension.get_data() != list(csr_extension)[0].get_data():
|
||||
return False
|
||||
return True
|
||||
elif self.backend == 'cryptography':
|
||||
if self.backend == 'cryptography':
|
||||
# Verify that CSR is signed by certificate's private key
|
||||
if not self.csr.is_signature_valid:
|
||||
return False
|
||||
@@ -244,10 +202,6 @@ class CertificateBackend(object):
|
||||
|
||||
def _check_subject_key_identifier(self):
|
||||
"""Check whether Subject Key Identifier matches, assuming self.existing_certificate has been populated."""
|
||||
if self.backend != 'cryptography':
|
||||
# We do not support SKI with pyOpenSSL backend
|
||||
return True
|
||||
|
||||
# Get hold of certificate's SKI
|
||||
try:
|
||||
ext = self.existing_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
||||
@@ -270,7 +224,7 @@ class CertificateBackend(object):
|
||||
return False
|
||||
return True
|
||||
|
||||
def needs_regeneration(self):
|
||||
def needs_regeneration(self, not_before=None, not_after=None):
|
||||
"""Check whether a regeneration is necessary."""
|
||||
if self.force or self.existing_certificate_bytes is None:
|
||||
return True
|
||||
@@ -294,6 +248,15 @@ class CertificateBackend(object):
|
||||
if self.create_subject_key_identifier != 'never_create' and not self._check_subject_key_identifier():
|
||||
return True
|
||||
|
||||
# Check not before
|
||||
if not_before is not None and not self.ignore_timestamps:
|
||||
if self.existing_certificate.not_valid_before != not_before:
|
||||
return True
|
||||
|
||||
# Check not after
|
||||
if not_after is not None and not self.ignore_timestamps:
|
||||
if self.existing_certificate.not_valid_after != not_after:
|
||||
return True
|
||||
return False
|
||||
|
||||
def dump(self, include_certificate):
|
||||
@@ -328,10 +291,6 @@ class CertificateProvider(object):
|
||||
def needs_version_two_certs(self, module):
|
||||
"""Whether the provider needs to create a version 2 certificate."""
|
||||
|
||||
def needs_pyopenssl_get_extensions(self, module):
|
||||
"""Whether the provider needs to use get_extensions() with pyOpenSSL."""
|
||||
return True
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_backend(self, module, backend):
|
||||
"""Create an implementation for a backend.
|
||||
@@ -352,45 +311,22 @@ def select_backend(module, backend, provider):
|
||||
if backend == 'auto':
|
||||
# Detect what backend we can use
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# If cryptography is available we'll use it
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
if provider.needs_version_two_certs(module):
|
||||
module.warn('crypto backend forced to pyopenssl. The cryptography library does not support v2 certificates')
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Fail if no backend has been found
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
module.fail_json(msg=("Can't detect the required Python library "
|
||||
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
if provider.needs_pyopenssl_get_extensions(module):
|
||||
try:
|
||||
getattr(crypto.X509Req, 'get_extensions')
|
||||
except AttributeError:
|
||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15')
|
||||
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
if provider.needs_version_two_certs(module):
|
||||
module.fail_json(msg='The cryptography backend does not support v2 certificates, '
|
||||
'use select_crypto_backend=pyopenssl for v2 certificates')
|
||||
module.fail_json(msg='The cryptography backend does not support v2 certificates')
|
||||
|
||||
return provider.create_backend(module, backend)
|
||||
|
||||
@@ -402,7 +338,8 @@ def get_certificate_argument_spec():
|
||||
force=dict(type='bool', default=False,),
|
||||
csr_path=dict(type='path'),
|
||||
csr_content=dict(type='str'),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
ignore_timestamps=dict(type='bool', default=True),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
|
||||
|
||||
# General properties of a certificate
|
||||
privatekey_path=dict(type='path'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,664 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import datetime
|
||||
|
||||
from ansible.module_utils._text import to_native, to_bytes, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
parse_name_field,
|
||||
get_relative_time_option,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_compare_public_keys,
|
||||
cryptography_get_name,
|
||||
cryptography_name_to_oid,
|
||||
cryptography_parse_key_usage_params,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
|
||||
pyopenssl_normalize_name_attribute,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
CertificateBackend,
|
||||
CertificateProvider,
|
||||
)
|
||||
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.x509 import NameAttribute, Name
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def compare_sets(subset, superset, equality=False):
|
||||
if equality:
|
||||
return set(subset) == set(superset)
|
||||
else:
|
||||
return all(x in superset for x in subset)
|
||||
|
||||
|
||||
def compare_dicts(subset, superset, equality=False):
|
||||
if equality:
|
||||
return subset == superset
|
||||
else:
|
||||
return all(superset.get(x) == v for x, v in subset.items())
|
||||
|
||||
|
||||
NO_EXTENSION = 'no extension'
|
||||
|
||||
|
||||
class AssertOnlyCertificateBackend(CertificateBackend):
|
||||
def __init__(self, module, backend):
|
||||
super(AssertOnlyCertificateBackend, self).__init__(module, backend)
|
||||
|
||||
self.signature_algorithms = module.params['signature_algorithms']
|
||||
if module.params['subject']:
|
||||
self.subject = parse_name_field(module.params['subject'])
|
||||
else:
|
||||
self.subject = []
|
||||
self.subject_strict = module.params['subject_strict']
|
||||
if module.params['issuer']:
|
||||
self.issuer = parse_name_field(module.params['issuer'])
|
||||
else:
|
||||
self.issuer = []
|
||||
self.issuer_strict = module.params['issuer_strict']
|
||||
self.has_expired = module.params['has_expired']
|
||||
self.version = module.params['version']
|
||||
self.key_usage = module.params['key_usage']
|
||||
self.key_usage_strict = module.params['key_usage_strict']
|
||||
self.extended_key_usage = module.params['extended_key_usage']
|
||||
self.extended_key_usage_strict = module.params['extended_key_usage_strict']
|
||||
self.subject_alt_name = module.params['subject_alt_name']
|
||||
self.subject_alt_name_strict = module.params['subject_alt_name_strict']
|
||||
self.not_before = module.params['not_before']
|
||||
self.not_after = module.params['not_after']
|
||||
self.valid_at = module.params['valid_at']
|
||||
self.invalid_at = module.params['invalid_at']
|
||||
self.valid_in = module.params['valid_in']
|
||||
if self.valid_in and not self.valid_in.startswith("+") and not self.valid_in.startswith("-"):
|
||||
try:
|
||||
int(self.valid_in)
|
||||
except ValueError:
|
||||
module.fail_json(msg='The supplied value for "valid_in" (%s) is not an integer or a valid timespec' % self.valid_in)
|
||||
self.valid_in = "+" + self.valid_in + "s"
|
||||
|
||||
# Load objects
|
||||
self._ensure_private_key_loaded()
|
||||
self._ensure_csr_loaded()
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_privatekey(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_csr_signature(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_csr_subject(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_csr_extensions(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_signature_algorithms(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_subject(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_issuer(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_has_expired(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_version(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_extended_key_usage(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_subject_alt_name(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_not_before(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_not_after(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_valid_at(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_invalid_at(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _validate_valid_in(self):
|
||||
pass
|
||||
|
||||
def assertonly(self):
|
||||
messages = []
|
||||
if self.privatekey_path is not None or self.privatekey_content is not None:
|
||||
if not self._validate_privatekey():
|
||||
messages.append(
|
||||
'Certificate and private key %s do not match' %
|
||||
(self.privatekey_path or '(provided in module options)')
|
||||
)
|
||||
|
||||
if self.csr_path is not None or self.csr_content is not None:
|
||||
if not self._validate_csr_signature():
|
||||
messages.append(
|
||||
'Certificate and CSR %s do not match: private key mismatch' %
|
||||
(self.csr_path or '(provided in module options)')
|
||||
)
|
||||
if not self._validate_csr_subject():
|
||||
messages.append(
|
||||
'Certificate and CSR %s do not match: subject mismatch' %
|
||||
(self.csr_path or '(provided in module options)')
|
||||
)
|
||||
if not self._validate_csr_extensions():
|
||||
messages.append(
|
||||
'Certificate and CSR %s do not match: extensions mismatch' %
|
||||
(self.csr_path or '(provided in module options)')
|
||||
)
|
||||
|
||||
if self.signature_algorithms is not None:
|
||||
wrong_alg = self._validate_signature_algorithms()
|
||||
if wrong_alg:
|
||||
messages.append(
|
||||
'Invalid signature algorithm (got %s, expected one of %s)' %
|
||||
(wrong_alg, self.signature_algorithms)
|
||||
)
|
||||
|
||||
if self.subject is not None:
|
||||
failure = self._validate_subject()
|
||||
if failure:
|
||||
dummy, cert_subject = failure
|
||||
messages.append(
|
||||
'Invalid subject component (got %s, expected all of %s to be present)' %
|
||||
(cert_subject, self.subject)
|
||||
)
|
||||
|
||||
if self.issuer is not None:
|
||||
failure = self._validate_issuer()
|
||||
if failure:
|
||||
dummy, cert_issuer = failure
|
||||
messages.append(
|
||||
'Invalid issuer component (got %s, expected all of %s to be present)' % (cert_issuer, self.issuer)
|
||||
)
|
||||
|
||||
if self.has_expired is not None:
|
||||
cert_expired = self._validate_has_expired()
|
||||
if cert_expired != self.has_expired:
|
||||
messages.append(
|
||||
'Certificate expiration check failed (certificate expiration is %s, expected %s)' %
|
||||
(cert_expired, self.has_expired)
|
||||
)
|
||||
|
||||
if self.version is not None:
|
||||
cert_version = self._validate_version()
|
||||
if cert_version != self.version:
|
||||
messages.append(
|
||||
'Invalid certificate version number (got %s, expected %s)' %
|
||||
(cert_version, self.version)
|
||||
)
|
||||
|
||||
if self.key_usage is not None:
|
||||
failure = self._validate_key_usage()
|
||||
if failure == NO_EXTENSION:
|
||||
messages.append('Found no keyUsage extension')
|
||||
elif failure:
|
||||
dummy, cert_key_usage = failure
|
||||
messages.append(
|
||||
'Invalid keyUsage components (got %s, expected all of %s to be present)' %
|
||||
(cert_key_usage, self.key_usage)
|
||||
)
|
||||
|
||||
if self.extended_key_usage is not None:
|
||||
failure = self._validate_extended_key_usage()
|
||||
if failure == NO_EXTENSION:
|
||||
messages.append('Found no extendedKeyUsage extension')
|
||||
elif failure:
|
||||
dummy, ext_cert_key_usage = failure
|
||||
messages.append(
|
||||
'Invalid extendedKeyUsage component (got %s, expected all of %s to be present)' % (ext_cert_key_usage, self.extended_key_usage)
|
||||
)
|
||||
|
||||
if self.subject_alt_name is not None:
|
||||
failure = self._validate_subject_alt_name()
|
||||
if failure == NO_EXTENSION:
|
||||
messages.append('Found no subjectAltName extension')
|
||||
elif failure:
|
||||
dummy, cert_san = failure
|
||||
messages.append(
|
||||
'Invalid subjectAltName component (got %s, expected all of %s to be present)' %
|
||||
(cert_san, self.subject_alt_name)
|
||||
)
|
||||
|
||||
if self.not_before is not None:
|
||||
cert_not_valid_before = self._validate_not_before()
|
||||
if cert_not_valid_before != get_relative_time_option(self.not_before, 'not_before', backend=self.backend):
|
||||
messages.append(
|
||||
'Invalid not_before component (got %s, expected %s to be present)' %
|
||||
(cert_not_valid_before, self.not_before)
|
||||
)
|
||||
|
||||
if self.not_after is not None:
|
||||
cert_not_valid_after = self._validate_not_after()
|
||||
if cert_not_valid_after != get_relative_time_option(self.not_after, 'not_after', backend=self.backend):
|
||||
messages.append(
|
||||
'Invalid not_after component (got %s, expected %s to be present)' %
|
||||
(cert_not_valid_after, self.not_after)
|
||||
)
|
||||
|
||||
if self.valid_at is not None:
|
||||
not_before, valid_at, not_after = self._validate_valid_at()
|
||||
if not (not_before <= valid_at <= not_after):
|
||||
messages.append(
|
||||
'Certificate is not valid for the specified date (%s) - not_before: %s - not_after: %s' %
|
||||
(self.valid_at, not_before, not_after)
|
||||
)
|
||||
|
||||
if self.invalid_at is not None:
|
||||
not_before, invalid_at, not_after = self._validate_invalid_at()
|
||||
if not_before <= invalid_at <= not_after:
|
||||
messages.append(
|
||||
'Certificate is not invalid for the specified date (%s) - not_before: %s - not_after: %s' %
|
||||
(self.invalid_at, not_before, not_after)
|
||||
)
|
||||
|
||||
if self.valid_in is not None:
|
||||
not_before, valid_in, not_after = self._validate_valid_in()
|
||||
if not not_before <= valid_in <= not_after:
|
||||
messages.append(
|
||||
'Certificate is not valid in %s from now (that would be %s) - not_before: %s - not_after: %s' %
|
||||
(self.valid_in, valid_in, not_before, not_after)
|
||||
)
|
||||
return messages
|
||||
|
||||
def needs_regeneration(self):
|
||||
self._ensure_existing_certificate_loaded()
|
||||
if self.existing_certificate is None:
|
||||
self.messages = ['Certificate not provided']
|
||||
else:
|
||||
self.messages = self.assertonly()
|
||||
|
||||
return len(self.messages) != 0
|
||||
|
||||
def generate_certificate(self):
|
||||
self.module.fail_json(msg=' | '.join(self.messages))
|
||||
|
||||
def get_certificate_data(self):
|
||||
return self.existing_certificate_bytes
|
||||
|
||||
|
||||
class AssertOnlyCertificateBackendCryptography(AssertOnlyCertificateBackend):
|
||||
"""Validate the supplied cert, using the cryptography backend"""
|
||||
def __init__(self, module):
|
||||
super(AssertOnlyCertificateBackendCryptography, self).__init__(module, 'cryptography')
|
||||
|
||||
def _validate_privatekey(self):
|
||||
return cryptography_compare_public_keys(self.existing_certificate.public_key(), self.privatekey.public_key())
|
||||
|
||||
def _validate_csr_signature(self):
|
||||
if not self.csr.is_signature_valid:
|
||||
return False
|
||||
return cryptography_compare_public_keys(self.csr.public_key(), self.existing_certificate.public_key())
|
||||
|
||||
def _validate_csr_subject(self):
|
||||
return self.csr.subject == self.existing_certificate.subject
|
||||
|
||||
def _validate_csr_extensions(self):
|
||||
cert_exts = self.existing_certificate.extensions
|
||||
csr_exts = self.csr.extensions
|
||||
if len(cert_exts) != len(csr_exts):
|
||||
return False
|
||||
for cert_ext in cert_exts:
|
||||
try:
|
||||
csr_ext = csr_exts.get_extension_for_oid(cert_ext.oid)
|
||||
if cert_ext != csr_ext:
|
||||
return False
|
||||
except cryptography.x509.ExtensionNotFound as dummy:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _validate_signature_algorithms(self):
|
||||
if self.existing_certificate.signature_algorithm_oid._name not in self.signature_algorithms:
|
||||
return self.existing_certificate.signature_algorithm_oid._name
|
||||
|
||||
def _validate_subject(self):
|
||||
expected_subject = Name([NameAttribute(oid=cryptography_name_to_oid(sub[0]), value=to_text(sub[1]))
|
||||
for sub in self.subject])
|
||||
cert_subject = self.existing_certificate.subject
|
||||
if not compare_sets(expected_subject, cert_subject, self.subject_strict):
|
||||
return expected_subject, cert_subject
|
||||
|
||||
def _validate_issuer(self):
|
||||
expected_issuer = Name([NameAttribute(oid=cryptography_name_to_oid(iss[0]), value=to_text(iss[1]))
|
||||
for iss in self.issuer])
|
||||
cert_issuer = self.existing_certificate.issuer
|
||||
if not compare_sets(expected_issuer, cert_issuer, self.issuer_strict):
|
||||
return self.issuer, cert_issuer
|
||||
|
||||
def _validate_has_expired(self):
|
||||
cert_not_after = self.existing_certificate.not_valid_after
|
||||
cert_expired = cert_not_after < datetime.datetime.utcnow()
|
||||
return cert_expired
|
||||
|
||||
def _validate_version(self):
|
||||
if self.existing_certificate.version == x509.Version.v1:
|
||||
return 1
|
||||
if self.existing_certificate.version == x509.Version.v3:
|
||||
return 3
|
||||
return "unknown"
|
||||
|
||||
def _validate_key_usage(self):
|
||||
try:
|
||||
current_key_usage = self.existing_certificate.extensions.get_extension_for_class(x509.KeyUsage).value
|
||||
test_key_usage = dict(
|
||||
digital_signature=current_key_usage.digital_signature,
|
||||
content_commitment=current_key_usage.content_commitment,
|
||||
key_encipherment=current_key_usage.key_encipherment,
|
||||
data_encipherment=current_key_usage.data_encipherment,
|
||||
key_agreement=current_key_usage.key_agreement,
|
||||
key_cert_sign=current_key_usage.key_cert_sign,
|
||||
crl_sign=current_key_usage.crl_sign,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
)
|
||||
if test_key_usage['key_agreement']:
|
||||
test_key_usage.update(dict(
|
||||
encipher_only=current_key_usage.encipher_only,
|
||||
decipher_only=current_key_usage.decipher_only
|
||||
))
|
||||
|
||||
key_usages = cryptography_parse_key_usage_params(self.key_usage)
|
||||
if not compare_dicts(key_usages, test_key_usage, self.key_usage_strict):
|
||||
return self.key_usage, [k for k, v in test_key_usage.items() if v is True]
|
||||
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.key_usage:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_extended_key_usage(self):
|
||||
try:
|
||||
current_ext_keyusage = self.existing_certificate.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value
|
||||
usages = [cryptography_name_to_oid(usage) for usage in self.extended_key_usage]
|
||||
expected_ext_keyusage = x509.ExtendedKeyUsage(usages)
|
||||
if not compare_sets(expected_ext_keyusage, current_ext_keyusage, self.extended_key_usage_strict):
|
||||
return [eku.value for eku in expected_ext_keyusage], [eku.value for eku in current_ext_keyusage]
|
||||
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.extended_key_usage:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_subject_alt_name(self):
|
||||
try:
|
||||
current_san = self.existing_certificate.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
|
||||
expected_san = [cryptography_get_name(san) for san in self.subject_alt_name]
|
||||
if not compare_sets(expected_san, current_san, self.subject_alt_name_strict):
|
||||
return self.subject_alt_name, current_san
|
||||
except cryptography.x509.ExtensionNotFound:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.subject_alt_name:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_not_before(self):
|
||||
return self.existing_certificate.not_valid_before
|
||||
|
||||
def _validate_not_after(self):
|
||||
return self.existing_certificate.not_valid_after
|
||||
|
||||
def _validate_valid_at(self):
|
||||
rt = get_relative_time_option(self.valid_at, 'valid_at', backend=self.backend)
|
||||
return self.existing_certificate.not_valid_before, rt, self.existing_certificate.not_valid_after
|
||||
|
||||
def _validate_invalid_at(self):
|
||||
rt = get_relative_time_option(self.invalid_at, 'invalid_at', backend=self.backend)
|
||||
return self.existing_certificate.not_valid_before, rt, self.existing_certificate.not_valid_after
|
||||
|
||||
def _validate_valid_in(self):
|
||||
valid_in_date = get_relative_time_option(self.valid_in, "valid_in", backend=self.backend)
|
||||
return self.existing_certificate.not_valid_before, valid_in_date, self.existing_certificate.not_valid_after
|
||||
|
||||
|
||||
class AssertOnlyCertificateBackendPyOpenSSL(AssertOnlyCertificateBackend):
|
||||
"""validate the supplied certificate."""
|
||||
|
||||
def __init__(self, module):
|
||||
super(AssertOnlyCertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
# Ensure inputs are properly sanitized before comparison.
|
||||
for param in ['signature_algorithms', 'key_usage', 'extended_key_usage',
|
||||
'subject_alt_name', 'subject', 'issuer', 'not_before',
|
||||
'not_after', 'valid_at', 'invalid_at']:
|
||||
attr = getattr(self, param)
|
||||
if isinstance(attr, list) and attr:
|
||||
if isinstance(attr[0], str):
|
||||
setattr(self, param, [to_bytes(item) for item in attr])
|
||||
elif isinstance(attr[0], tuple):
|
||||
setattr(self, param, [(to_bytes(item[0]), to_bytes(item[1])) for item in attr])
|
||||
elif isinstance(attr, tuple):
|
||||
setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items()))
|
||||
elif isinstance(attr, dict):
|
||||
setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items()))
|
||||
elif isinstance(attr, str):
|
||||
setattr(self, param, to_bytes(attr))
|
||||
|
||||
def _validate_privatekey(self):
|
||||
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD)
|
||||
ctx.use_privatekey(self.privatekey)
|
||||
ctx.use_certificate(self.existing_certificate)
|
||||
try:
|
||||
ctx.check_privatekey()
|
||||
return True
|
||||
except OpenSSL.SSL.Error:
|
||||
return False
|
||||
|
||||
def _validate_csr_signature(self):
|
||||
try:
|
||||
self.csr.verify(self.existing_certificate.get_pubkey())
|
||||
except OpenSSL.crypto.Error:
|
||||
return False
|
||||
|
||||
def _validate_csr_subject(self):
|
||||
if self.csr.get_subject() != self.existing_certificate.get_subject():
|
||||
return False
|
||||
|
||||
def _validate_csr_extensions(self):
|
||||
csr_extensions = self.csr.get_extensions()
|
||||
cert_extension_count = self.existing_certificate.get_extension_count()
|
||||
if len(csr_extensions) != cert_extension_count:
|
||||
return False
|
||||
for extension_number in range(0, cert_extension_count):
|
||||
cert_extension = self.existing_certificate.get_extension(extension_number)
|
||||
csr_extension = filter(lambda extension: extension.get_short_name() == cert_extension.get_short_name(), csr_extensions)
|
||||
if cert_extension.get_data() != list(csr_extension)[0].get_data():
|
||||
return False
|
||||
return True
|
||||
|
||||
def _validate_signature_algorithms(self):
|
||||
if self.existing_certificate.get_signature_algorithm() not in self.signature_algorithms:
|
||||
return self.existing_certificate.get_signature_algorithm()
|
||||
|
||||
def _validate_subject(self):
|
||||
expected_subject = [(OpenSSL._util.lib.OBJ_txt2nid(sub[0]), sub[1]) for sub in self.subject]
|
||||
cert_subject = self.existing_certificate.get_subject().get_components()
|
||||
current_subject = [(OpenSSL._util.lib.OBJ_txt2nid(sub[0]), sub[1]) for sub in cert_subject]
|
||||
if not compare_sets(expected_subject, current_subject, self.subject_strict):
|
||||
return expected_subject, current_subject
|
||||
|
||||
def _validate_issuer(self):
|
||||
expected_issuer = [(OpenSSL._util.lib.OBJ_txt2nid(iss[0]), iss[1]) for iss in self.issuer]
|
||||
cert_issuer = self.existing_certificate.get_issuer().get_components()
|
||||
current_issuer = [(OpenSSL._util.lib.OBJ_txt2nid(iss[0]), iss[1]) for iss in cert_issuer]
|
||||
if not compare_sets(expected_issuer, current_issuer, self.issuer_strict):
|
||||
return self.issuer, cert_issuer
|
||||
|
||||
def _validate_has_expired(self):
|
||||
# The following 3 lines are the same as the current PyOpenSSL code for cert.has_expired().
|
||||
# Older version of PyOpenSSL have a buggy implementation,
|
||||
# to avoid issues with those we added the code from a more recent release here.
|
||||
|
||||
time_string = to_native(self.existing_certificate.get_notAfter())
|
||||
not_after = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||
cert_expired = not_after < datetime.datetime.utcnow()
|
||||
return cert_expired
|
||||
|
||||
def _validate_version(self):
|
||||
# Version numbers in certs are off by one:
|
||||
# v1: 0, v2: 1, v3: 2 ...
|
||||
return self.existing_certificate.get_version() + 1
|
||||
|
||||
def _validate_key_usage(self):
|
||||
found = False
|
||||
for extension_idx in range(0, self.existing_certificate.get_extension_count()):
|
||||
extension = self.existing_certificate.get_extension(extension_idx)
|
||||
if extension.get_short_name() == b'keyUsage':
|
||||
found = True
|
||||
expected_extension = crypto.X509Extension(b"keyUsage", False, b', '.join(self.key_usage))
|
||||
key_usage = [usage.strip() for usage in to_text(expected_extension, errors='surrogate_or_strict').split(',')]
|
||||
current_ku = [usage.strip() for usage in to_text(extension, errors='surrogate_or_strict').split(',')]
|
||||
if not compare_sets(key_usage, current_ku, self.key_usage_strict):
|
||||
return self.key_usage, str(extension).split(', ')
|
||||
if not found:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.key_usage:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_extended_key_usage(self):
|
||||
found = False
|
||||
for extension_idx in range(0, self.existing_certificate.get_extension_count()):
|
||||
extension = self.existing_certificate.get_extension(extension_idx)
|
||||
if extension.get_short_name() == b'extendedKeyUsage':
|
||||
found = True
|
||||
extKeyUsage = [OpenSSL._util.lib.OBJ_txt2nid(keyUsage) for keyUsage in self.extended_key_usage]
|
||||
current_xku = [OpenSSL._util.lib.OBJ_txt2nid(usage.strip()) for usage in
|
||||
to_bytes(extension, errors='surrogate_or_strict').split(b',')]
|
||||
if not compare_sets(extKeyUsage, current_xku, self.extended_key_usage_strict):
|
||||
return self.extended_key_usage, str(extension).split(', ')
|
||||
if not found:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.extended_key_usage:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_subject_alt_name(self):
|
||||
found = False
|
||||
for extension_idx in range(0, self.existing_certificate.get_extension_count()):
|
||||
extension = self.existing_certificate.get_extension(extension_idx)
|
||||
if extension.get_short_name() == b'subjectAltName':
|
||||
found = True
|
||||
l_altnames = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
|
||||
to_text(extension, errors='surrogate_or_strict').split(', ')]
|
||||
sans = [pyopenssl_normalize_name_attribute(to_text(san, errors='surrogate_or_strict')) for san in self.subject_alt_name]
|
||||
if not compare_sets(sans, l_altnames, self.subject_alt_name_strict):
|
||||
return self.subject_alt_name, l_altnames
|
||||
if not found:
|
||||
# This is only bad if the user specified a non-empty list
|
||||
if self.subject_alt_name:
|
||||
return NO_EXTENSION
|
||||
|
||||
def _validate_not_before(self):
|
||||
return self.existing_certificate.get_notBefore()
|
||||
|
||||
def _validate_not_after(self):
|
||||
return self.existing_certificate.get_notAfter()
|
||||
|
||||
def _validate_valid_at(self):
|
||||
rt = get_relative_time_option(self.valid_at, "valid_at", backend=self.backend)
|
||||
rt = to_bytes(rt, errors='surrogate_or_strict')
|
||||
return self.existing_certificate.get_notBefore(), rt, self.existing_certificate.get_notAfter()
|
||||
|
||||
def _validate_invalid_at(self):
|
||||
rt = get_relative_time_option(self.invalid_at, "invalid_at", backend=self.backend)
|
||||
rt = to_bytes(rt, errors='surrogate_or_strict')
|
||||
return self.existing_certificate.get_notBefore(), rt, self.existing_certificate.get_notAfter()
|
||||
|
||||
def _validate_valid_in(self):
|
||||
valid_in_asn1 = get_relative_time_option(self.valid_in, "valid_in", backend=self.backend)
|
||||
valid_in_date = to_bytes(valid_in_asn1, errors='surrogate_or_strict')
|
||||
return self.existing_certificate.get_notBefore(), valid_in_date, self.existing_certificate.get_notAfter()
|
||||
|
||||
|
||||
class AssertOnlyCertificateProvider(CertificateProvider):
|
||||
def validate_module_args(self, module):
|
||||
module.deprecate("The 'assertonly' provider is deprecated; please see the examples of "
|
||||
"the 'x509_certificate' module on how to replace it with other modules",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
def needs_version_two_certs(self, module):
|
||||
return False
|
||||
|
||||
def create_backend(self, module, backend):
|
||||
if backend == 'cryptography':
|
||||
return AssertOnlyCertificateBackendCryptography(module)
|
||||
if backend == 'pyopenssl':
|
||||
return AssertOnlyCertificateBackendPyOpenSSL(module)
|
||||
|
||||
|
||||
def add_assertonly_provider_to_argument_spec(argument_spec):
|
||||
argument_spec.argument_spec['provider']['choices'].append('assertonly')
|
||||
argument_spec.argument_spec.update(dict(
|
||||
signature_algorithms=dict(type='list', elements='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
subject=dict(type='dict', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
subject_strict=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
issuer=dict(type='dict', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
issuer_strict=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
has_expired=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
version=dict(type='int', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
key_usage=dict(type='list', elements='str', aliases=['keyUsage'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
key_usage_strict=dict(type='bool', default=False, aliases=['keyUsage_strict'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
extended_key_usage=dict(type='list', elements='str', aliases=['extendedKeyUsage'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
extended_key_usage_strict=dict(type='bool', default=False, aliases=['extendedKeyUsage_strict'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
subject_alt_name_strict=dict(type='bool', default=False, aliases=['subjectAltName_strict'],
|
||||
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
not_before=dict(type='str', aliases=['notBefore'], removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
not_after=dict(type='str', aliases=['notAfter'], removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
valid_at=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
invalid_at=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
valid_in=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
|
||||
))
|
||||
@@ -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
|
||||
|
||||
@@ -58,18 +58,7 @@ class EntrustCertificateBackend(CertificateBackend):
|
||||
# We want to always force behavior of trying to use the organization provided in the CSR.
|
||||
# To that end we need to parse out the organization from the CSR.
|
||||
self.csr_org = None
|
||||
if self.backend == 'pyopenssl':
|
||||
csr_subject = self.csr.get_subject()
|
||||
csr_subject_components = csr_subject.get_components()
|
||||
for k, v in csr_subject_components:
|
||||
if k.upper() == 'O':
|
||||
# Entrust does not support multiple validated organizations in a single certificate
|
||||
if self.csr_org is not None:
|
||||
self.module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations "
|
||||
"found in Subject DN: '{0}'. ".format(csr_subject)))
|
||||
else:
|
||||
self.csr_org = v
|
||||
elif self.backend == 'cryptography':
|
||||
if self.backend == 'cryptography':
|
||||
csr_subject_orgs = self.csr.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)
|
||||
if len(csr_subject_orgs) == 1:
|
||||
self.csr_org = csr_subject_orgs[0].value
|
||||
@@ -162,11 +151,7 @@ class EntrustCertificateBackend(CertificateBackend):
|
||||
if self.existing_certificate:
|
||||
serial_number = None
|
||||
expiry = None
|
||||
if self.backend == 'pyopenssl':
|
||||
serial_number = "{0:X}".format(self.existing_certificate.get_serial_number())
|
||||
time_string = to_native(self.existing_certificate.get_notAfter())
|
||||
expiry = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||
elif self.backend == 'cryptography':
|
||||
if self.backend == 'cryptography':
|
||||
serial_number = "{0:X}".format(cryptography_serial_number_of_cert(self.existing_certificate))
|
||||
expiry = self.existing_certificate.not_valid_after
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ 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.crypto.support import (
|
||||
load_certificate,
|
||||
@@ -33,37 +33,11 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
||||
cryptography_serial_number_of_cert,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
|
||||
pyopenssl_get_extensions_from_cert,
|
||||
pyopenssl_normalize_name,
|
||||
pyopenssl_normalize_name_attribute,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||
get_publickey_info,
|
||||
)
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# OpenSSL 1.1.0 or newer
|
||||
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
|
||||
else:
|
||||
# OpenSSL 1.0.x or older
|
||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
@@ -209,20 +183,19 @@ class CertificateInfoRetrieval(object):
|
||||
result['fingerprints'] = get_fingerprint_of_bytes(
|
||||
self._get_der_bytes(), prefer_one=prefer_one_fingerprint)
|
||||
|
||||
if self.backend != 'pyopenssl':
|
||||
ski = self._get_subject_key_identifier()
|
||||
if ski is not None:
|
||||
ski = to_native(binascii.hexlify(ski))
|
||||
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
|
||||
result['subject_key_identifier'] = ski
|
||||
ski = self._get_subject_key_identifier()
|
||||
if ski is not None:
|
||||
ski = to_native(binascii.hexlify(ski))
|
||||
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
|
||||
result['subject_key_identifier'] = ski
|
||||
|
||||
aki, aci, acsn = self._get_authority_key_identifier()
|
||||
if aki is not None:
|
||||
aki = to_native(binascii.hexlify(aki))
|
||||
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
||||
result['authority_key_identifier'] = aki
|
||||
result['authority_cert_issuer'] = aci
|
||||
result['authority_cert_serial_number'] = acsn
|
||||
aki, aci, acsn = self._get_authority_key_identifier()
|
||||
if aki is not None:
|
||||
aki = to_native(binascii.hexlify(aki))
|
||||
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
||||
result['authority_key_identifier'] = aki
|
||||
result['authority_cert_issuer'] = aci
|
||||
result['authority_cert_serial_number'] = acsn
|
||||
|
||||
result['serial_number'] = self._get_serial_number()
|
||||
result['extensions_by_oid'] = self._get_all_extensions()
|
||||
@@ -392,136 +365,9 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
|
||||
return None
|
||||
|
||||
|
||||
class CertificateInfoRetrievalPyOpenSSL(CertificateInfoRetrieval):
|
||||
"""validate the supplied certificate."""
|
||||
|
||||
def __init__(self, module, content):
|
||||
super(CertificateInfoRetrievalPyOpenSSL, self).__init__(module, 'pyopenssl', content)
|
||||
|
||||
def _get_der_bytes(self):
|
||||
return crypto.dump_certificate(crypto.FILETYPE_ASN1, self.cert)
|
||||
|
||||
def _get_signature_algorithm(self):
|
||||
return to_text(self.cert.get_signature_algorithm())
|
||||
|
||||
def __get_name(self, name):
|
||||
result = []
|
||||
for sub in name.get_components():
|
||||
result.append([pyopenssl_normalize_name(sub[0]), to_text(sub[1])])
|
||||
return result
|
||||
|
||||
def _get_subject_ordered(self):
|
||||
return self.__get_name(self.cert.get_subject())
|
||||
|
||||
def _get_issuer_ordered(self):
|
||||
return self.__get_name(self.cert.get_issuer())
|
||||
|
||||
def _get_version(self):
|
||||
# Version numbers in certs are off by one:
|
||||
# v1: 0, v2: 1, v3: 2 ...
|
||||
return self.cert.get_version() + 1
|
||||
|
||||
def _get_extension(self, short_name):
|
||||
for extension_idx in range(0, self.cert.get_extension_count()):
|
||||
extension = self.cert.get_extension(extension_idx)
|
||||
if extension.get_short_name() == short_name:
|
||||
result = [
|
||||
pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',')
|
||||
]
|
||||
return sorted(result), bool(extension.get_critical())
|
||||
return None, False
|
||||
|
||||
def _get_key_usage(self):
|
||||
return self._get_extension(b'keyUsage')
|
||||
|
||||
def _get_extended_key_usage(self):
|
||||
return self._get_extension(b'extendedKeyUsage')
|
||||
|
||||
def _get_basic_constraints(self):
|
||||
return self._get_extension(b'basicConstraints')
|
||||
|
||||
def _get_ocsp_must_staple(self):
|
||||
extensions = [self.cert.get_extension(i) for i in range(0, self.cert.get_extension_count())]
|
||||
oms_ext = [
|
||||
ext for ext in extensions
|
||||
if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE
|
||||
]
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
|
||||
# Older versions of libssl don't know about OCSP Must Staple
|
||||
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
|
||||
if oms_ext:
|
||||
return True, bool(oms_ext[0].get_critical())
|
||||
else:
|
||||
return None, False
|
||||
|
||||
def _get_subject_alt_name(self):
|
||||
for extension_idx in range(0, self.cert.get_extension_count()):
|
||||
extension = self.cert.get_extension(extension_idx)
|
||||
if extension.get_short_name() == b'subjectAltName':
|
||||
result = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
|
||||
to_text(extension, errors='surrogate_or_strict').split(', ')]
|
||||
return result, bool(extension.get_critical())
|
||||
return None, False
|
||||
|
||||
def get_not_before(self):
|
||||
time_string = to_native(self.cert.get_notBefore())
|
||||
return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||
|
||||
def get_not_after(self):
|
||||
time_string = to_native(self.cert.get_notAfter())
|
||||
return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
||||
|
||||
def _get_public_key_pem(self):
|
||||
try:
|
||||
return crypto.dump_publickey(
|
||||
crypto.FILETYPE_PEM,
|
||||
self.cert.get_pubkey(),
|
||||
)
|
||||
except AttributeError:
|
||||
try:
|
||||
# pyOpenSSL < 16.0:
|
||||
bio = crypto._new_mem_buf()
|
||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.cert.get_pubkey()._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
return crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
||||
|
||||
def _get_public_key_object(self):
|
||||
return self.cert.get_pubkey()
|
||||
|
||||
def _get_subject_key_identifier(self):
|
||||
# Won't be implemented
|
||||
return None
|
||||
|
||||
def _get_authority_key_identifier(self):
|
||||
# Won't be implemented
|
||||
return None, None, None
|
||||
|
||||
def _get_serial_number(self):
|
||||
return self.cert.get_serial_number()
|
||||
|
||||
def _get_all_extensions(self):
|
||||
return pyopenssl_get_extensions_from_cert(self.cert)
|
||||
|
||||
def _get_ocsp_uri(self):
|
||||
for i in range(self.cert.get_extension_count()):
|
||||
ext = self.cert.get_extension(i)
|
||||
if ext.get_short_name() == b'authorityInfoAccess':
|
||||
v = str(ext)
|
||||
m = re.search('^OCSP - URI:(.*)$', v, flags=re.MULTILINE)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def get_certificate_info(module, backend, content, prefer_one_fingerprint=False):
|
||||
if backend == 'cryptography':
|
||||
info = CertificateInfoRetrievalCryptography(module, content)
|
||||
elif backend == 'pyopenssl':
|
||||
info = CertificateInfoRetrievalPyOpenSSL(module, content)
|
||||
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
|
||||
|
||||
|
||||
@@ -529,34 +375,17 @@ def select_backend(module, backend, content):
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# First try cryptography, then pyOpenSSL
|
||||
# Try cryptography
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
try:
|
||||
getattr(crypto.X509Req, 'get_extensions')
|
||||
except AttributeError:
|
||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
|
||||
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, CertificateInfoRetrievalPyOpenSSL(module, content)
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
|
||||
@@ -13,7 +13,7 @@ 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.crypto.basic import (
|
||||
OpenSSLBadPassphraseError,
|
||||
@@ -169,7 +169,7 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
|
||||
return self.cert.public_bytes(Encoding.PEM)
|
||||
|
||||
def needs_regeneration(self):
|
||||
if super(OwnCACertificateBackendCryptography, self).needs_regeneration():
|
||||
if super(OwnCACertificateBackendCryptography, self).needs_regeneration(not_before=self.notBefore, not_after=self.notAfter):
|
||||
return True
|
||||
|
||||
# Check AuthorityKeyIdentifier
|
||||
@@ -227,100 +227,6 @@ def generate_serial_number():
|
||||
return result
|
||||
|
||||
|
||||
class OwnCACertificateBackendPyOpenSSL(CertificateBackend):
|
||||
def __init__(self, module):
|
||||
super(OwnCACertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
self.notBefore = get_relative_time_option(self.module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend)
|
||||
self.notAfter = get_relative_time_option(self.module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend)
|
||||
self.digest = self.module.params['ownca_digest']
|
||||
self.version = self.module.params['ownca_version']
|
||||
self.serial_number = generate_serial_number()
|
||||
if self.module.params['ownca_create_subject_key_identifier'] != 'create_if_not_provided':
|
||||
self.module.fail_json(msg='ownca_create_subject_key_identifier cannot be used with the pyOpenSSL backend!')
|
||||
if self.module.params['ownca_create_authority_key_identifier']:
|
||||
self.module.warn('ownca_create_authority_key_identifier is ignored by the pyOpenSSL backend!')
|
||||
self.ca_cert_path = self.module.params['ownca_path']
|
||||
self.ca_cert_content = self.module.params['ownca_content']
|
||||
if self.ca_cert_content is not None:
|
||||
self.ca_cert_content = self.ca_cert_content.encode('utf-8')
|
||||
self.ca_privatekey_path = self.module.params['ownca_privatekey_path']
|
||||
self.ca_privatekey_content = self.module.params['ownca_privatekey_content']
|
||||
if self.ca_privatekey_content is not None:
|
||||
self.ca_privatekey_content = self.ca_privatekey_content.encode('utf-8')
|
||||
self.ca_privatekey_passphrase = self.module.params['ownca_privatekey_passphrase']
|
||||
|
||||
if self.csr_content is None and not os.path.exists(self.csr_path):
|
||||
raise CertificateError(
|
||||
'The certificate signing request file {0} does not exist'.format(self.csr_path)
|
||||
)
|
||||
if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path):
|
||||
raise CertificateError(
|
||||
'The CA certificate file {0} does not exist'.format(self.ca_cert_path)
|
||||
)
|
||||
if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path):
|
||||
raise CertificateError(
|
||||
'The CA private key file {0} does not exist'.format(self.ca_privatekey_path)
|
||||
)
|
||||
|
||||
self._ensure_csr_loaded()
|
||||
self.ca_cert = load_certificate(
|
||||
path=self.ca_cert_path,
|
||||
content=self.ca_cert_content,
|
||||
)
|
||||
try:
|
||||
self.ca_privatekey = load_privatekey(
|
||||
path=self.ca_privatekey_path,
|
||||
content=self.ca_privatekey_content,
|
||||
passphrase=self.ca_privatekey_passphrase
|
||||
)
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
self.module.fail_json(msg=str(exc))
|
||||
|
||||
def generate_certificate(self):
|
||||
"""(Re-)Generate certificate."""
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(self.serial_number)
|
||||
cert.set_notBefore(to_bytes(self.notBefore))
|
||||
cert.set_notAfter(to_bytes(self.notAfter))
|
||||
cert.set_subject(self.csr.get_subject())
|
||||
cert.set_issuer(self.ca_cert.get_subject())
|
||||
cert.set_version(self.version - 1)
|
||||
cert.set_pubkey(self.csr.get_pubkey())
|
||||
cert.add_extensions(self.csr.get_extensions())
|
||||
|
||||
cert.sign(self.ca_privatekey, self.digest)
|
||||
self.cert = cert
|
||||
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(OwnCACertificateBackendPyOpenSSL, self).dump(include_certificate)
|
||||
result.update({
|
||||
'ca_cert': self.ca_cert_path,
|
||||
'ca_privatekey': self.ca_privatekey_path,
|
||||
})
|
||||
|
||||
if self.module.check_mode:
|
||||
result.update({
|
||||
'notBefore': self.notBefore,
|
||||
'notAfter': self.notAfter,
|
||||
'serial_number': self.serial_number,
|
||||
})
|
||||
else:
|
||||
if self.cert is None:
|
||||
self.cert = self.existing_certificate
|
||||
result.update({
|
||||
'notBefore': self.cert.get_notBefore(),
|
||||
'notAfter': self.cert.get_notAfter(),
|
||||
'serial_number': self.cert.get_serial_number(),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class OwnCACertificateProvider(CertificateProvider):
|
||||
def validate_module_args(self, module):
|
||||
if module.params['ownca_path'] is None and module.params['ownca_content'] is None:
|
||||
@@ -334,8 +240,6 @@ class OwnCACertificateProvider(CertificateProvider):
|
||||
def create_backend(self, module, backend):
|
||||
if backend == 'cryptography':
|
||||
return OwnCACertificateBackendCryptography(module)
|
||||
if backend == 'pyopenssl':
|
||||
return OwnCACertificateBackendPyOpenSSL(module)
|
||||
|
||||
|
||||
def add_ownca_provider_to_argument_spec(argument_spec):
|
||||
|
||||
@@ -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,
|
||||
@@ -134,6 +134,10 @@ class SelfSignedCertificateBackendCryptography(CertificateBackend):
|
||||
"""Return bytes for self.cert."""
|
||||
return self.cert.public_bytes(Encoding.PEM)
|
||||
|
||||
def needs_regeneration(self):
|
||||
return super(SelfSignedCertificateBackendCryptography, self).needs_regeneration(
|
||||
not_before=self.notBefore, not_after=self.notAfter)
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(SelfSignedCertificateBackendCryptography, self).dump(include_certificate)
|
||||
|
||||
@@ -163,76 +167,6 @@ def generate_serial_number():
|
||||
return result
|
||||
|
||||
|
||||
class SelfSignedCertificateBackendPyOpenSSL(CertificateBackend):
|
||||
def __init__(self, module):
|
||||
super(SelfSignedCertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
if module.params['selfsigned_create_subject_key_identifier'] != 'create_if_not_provided':
|
||||
module.fail_json(msg='selfsigned_create_subject_key_identifier cannot be used with the pyOpenSSL backend!')
|
||||
self.notBefore = get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend)
|
||||
self.notAfter = get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend)
|
||||
self.digest = module.params['selfsigned_digest']
|
||||
self.version = module.params['selfsigned_version']
|
||||
self.serial_number = generate_serial_number()
|
||||
|
||||
if self.csr_path is not None and not os.path.exists(self.csr_path):
|
||||
raise CertificateError(
|
||||
'The certificate signing request file {0} does not exist'.format(self.csr_path)
|
||||
)
|
||||
if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
|
||||
raise CertificateError(
|
||||
'The private key file {0} does not exist'.format(self.privatekey_path)
|
||||
)
|
||||
|
||||
self._ensure_private_key_loaded()
|
||||
|
||||
self._ensure_csr_loaded()
|
||||
if self.csr is None:
|
||||
# Create empty CSR on the fly
|
||||
self.csr = crypto.X509Req()
|
||||
self.csr.set_pubkey(self.privatekey)
|
||||
self.csr.sign(self.privatekey, self.digest)
|
||||
|
||||
def generate_certificate(self):
|
||||
"""(Re-)Generate certificate."""
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(self.serial_number)
|
||||
cert.set_notBefore(to_bytes(self.notBefore))
|
||||
cert.set_notAfter(to_bytes(self.notAfter))
|
||||
cert.set_subject(self.csr.get_subject())
|
||||
cert.set_issuer(self.csr.get_subject())
|
||||
cert.set_version(self.version - 1)
|
||||
cert.set_pubkey(self.csr.get_pubkey())
|
||||
cert.add_extensions(self.csr.get_extensions())
|
||||
|
||||
cert.sign(self.privatekey, self.digest)
|
||||
self.cert = cert
|
||||
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(SelfSignedCertificateBackendPyOpenSSL, self).dump(include_certificate)
|
||||
|
||||
if self.module.check_mode:
|
||||
result.update({
|
||||
'notBefore': self.notBefore,
|
||||
'notAfter': self.notAfter,
|
||||
'serial_number': self.serial_number,
|
||||
})
|
||||
else:
|
||||
if self.cert is None:
|
||||
self.cert = self.existing_certificate
|
||||
result.update({
|
||||
'notBefore': self.cert.get_notBefore(),
|
||||
'notAfter': self.cert.get_notAfter(),
|
||||
'serial_number': self.cert.get_serial_number(),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class SelfSignedCertificateProvider(CertificateProvider):
|
||||
def validate_module_args(self, module):
|
||||
if module.params['privatekey_path'] is None and module.params['privatekey_content'] is None:
|
||||
@@ -244,8 +178,6 @@ class SelfSignedCertificateProvider(CertificateProvider):
|
||||
def create_backend(self, module, backend):
|
||||
if backend == 'cryptography':
|
||||
return SelfSignedCertificateBackendCryptography(module)
|
||||
if backend == 'pyopenssl':
|
||||
return SelfSignedCertificateBackendPyOpenSSL(module)
|
||||
|
||||
|
||||
def add_selfsigned_provider_to_argument_spec(argument_spec):
|
||||
|
||||
@@ -16,7 +16,7 @@ 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_native, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
@@ -27,6 +27,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
||||
load_privatekey,
|
||||
load_certificate_request,
|
||||
parse_name_field,
|
||||
parse_ordered_name_field,
|
||||
select_message_digest,
|
||||
)
|
||||
|
||||
@@ -43,11 +44,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
||||
REVOCATION_REASON_MAP,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
|
||||
pyopenssl_normalize_name_attribute,
|
||||
pyopenssl_parse_name_constraints,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import (
|
||||
get_csr_info,
|
||||
)
|
||||
@@ -55,28 +51,8 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
|
||||
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# OpenSSL 1.1.0 or newer
|
||||
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
|
||||
else:
|
||||
# OpenSSL 1.0.x or older
|
||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
@@ -144,6 +120,7 @@ class CertificateSigningRequestBackend(object):
|
||||
if self.create_subject_key_identifier and self.subject_key_identifier is not None:
|
||||
module.fail_json(msg='subject_key_identifier cannot be specified if create_subject_key_identifier is true')
|
||||
|
||||
self.ordered_subject = False
|
||||
self.subject = [
|
||||
('C', module.params['country_name']),
|
||||
('ST', module.params['state_or_province_name']),
|
||||
@@ -153,11 +130,19 @@ class CertificateSigningRequestBackend(object):
|
||||
('CN', module.params['common_name']),
|
||||
('emailAddress', module.params['email_address']),
|
||||
]
|
||||
|
||||
if module.params['subject']:
|
||||
self.subject = self.subject + parse_name_field(module.params['subject'])
|
||||
self.subject = [(entry[0], entry[1]) for entry in self.subject if entry[1]]
|
||||
|
||||
try:
|
||||
if module.params['subject']:
|
||||
self.subject = self.subject + parse_name_field(module.params['subject'], 'subject')
|
||||
if module.params['subject_ordered']:
|
||||
if self.subject:
|
||||
raise CertificateSigningRequestError('subject_ordered cannot be combined with any other subject field')
|
||||
self.subject = parse_ordered_name_field(module.params['subject_ordered'], 'subject_ordered')
|
||||
self.ordered_subject = True
|
||||
except ValueError as exc:
|
||||
raise CertificateSigningRequestError(to_native(exc))
|
||||
|
||||
self.using_common_name_for_san = False
|
||||
if not self.subjectAltName and module.params['use_common_name_for_san']:
|
||||
for sub in self.subject:
|
||||
@@ -273,174 +258,6 @@ class CertificateSigningRequestBackend(object):
|
||||
return result
|
||||
|
||||
|
||||
# Implementation with using pyOpenSSL
|
||||
class CertificateSigningRequestPyOpenSSLBackend(CertificateSigningRequestBackend):
|
||||
def __init__(self, module):
|
||||
for o in ('create_subject_key_identifier', ):
|
||||
if module.params[o]:
|
||||
module.fail_json(msg='You cannot use {0} with the pyOpenSSL backend!'.format(o))
|
||||
for o in ('subject_key_identifier', 'authority_key_identifier', 'authority_cert_issuer', 'authority_cert_serial_number', 'crl_distribution_points'):
|
||||
if module.params[o] is not None:
|
||||
module.fail_json(msg='You cannot use {0} with the pyOpenSSL backend!'.format(o))
|
||||
super(CertificateSigningRequestPyOpenSSLBackend, self).__init__(module, 'pyopenssl')
|
||||
|
||||
def generate_csr(self):
|
||||
"""(Re-)Generate CSR."""
|
||||
self._ensure_private_key_loaded()
|
||||
|
||||
req = crypto.X509Req()
|
||||
req.set_version(self.version - 1)
|
||||
subject = req.get_subject()
|
||||
for entry in self.subject:
|
||||
if entry[1] is not None:
|
||||
# Workaround for https://github.com/pyca/pyopenssl/issues/165
|
||||
nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(entry[0]))
|
||||
if nid == 0:
|
||||
raise CertificateSigningRequestError('Unknown subject field identifier "{0}"'.format(entry[0]))
|
||||
res = OpenSSL._util.lib.X509_NAME_add_entry_by_NID(subject._name, nid, OpenSSL._util.lib.MBSTRING_UTF8, to_bytes(entry[1]), -1, -1, 0)
|
||||
if res == 0:
|
||||
raise CertificateSigningRequestError('Invalid value for subject field identifier "{0}": {1}'.format(entry[0], entry[1]))
|
||||
|
||||
extensions = []
|
||||
if self.subjectAltName:
|
||||
altnames = ', '.join(self.subjectAltName)
|
||||
try:
|
||||
extensions.append(crypto.X509Extension(b"subjectAltName", self.subjectAltName_critical, altnames.encode('ascii')))
|
||||
except OpenSSL.crypto.Error as e:
|
||||
raise CertificateSigningRequestError(
|
||||
'Error while parsing Subject Alternative Names {0} (check for missing type prefix, such as "DNS:"!): {1}'.format(
|
||||
', '.join(["{0}".format(san) for san in self.subjectAltName]), str(e)
|
||||
)
|
||||
)
|
||||
|
||||
if self.keyUsage:
|
||||
usages = ', '.join(self.keyUsage)
|
||||
extensions.append(crypto.X509Extension(b"keyUsage", self.keyUsage_critical, usages.encode('ascii')))
|
||||
|
||||
if self.extendedKeyUsage:
|
||||
usages = ', '.join(self.extendedKeyUsage)
|
||||
extensions.append(crypto.X509Extension(b"extendedKeyUsage", self.extendedKeyUsage_critical, usages.encode('ascii')))
|
||||
|
||||
if self.basicConstraints:
|
||||
usages = ', '.join(self.basicConstraints)
|
||||
extensions.append(crypto.X509Extension(b"basicConstraints", self.basicConstraints_critical, usages.encode('ascii')))
|
||||
|
||||
if self.name_constraints_permitted or self.name_constraints_excluded:
|
||||
usages = ', '.join(
|
||||
['permitted;{0}'.format(name) for name in self.name_constraints_permitted] +
|
||||
['excluded;{0}'.format(name) for name in self.name_constraints_excluded]
|
||||
)
|
||||
extensions.append(crypto.X509Extension(b"nameConstraints", self.name_constraints_critical, usages.encode('ascii')))
|
||||
|
||||
if self.ocspMustStaple:
|
||||
extensions.append(crypto.X509Extension(OPENSSL_MUST_STAPLE_NAME, self.ocspMustStaple_critical, OPENSSL_MUST_STAPLE_VALUE))
|
||||
|
||||
if extensions:
|
||||
req.add_extensions(extensions)
|
||||
|
||||
req.set_pubkey(self.privatekey)
|
||||
req.sign(self.privatekey, self.digest)
|
||||
self.csr = req
|
||||
|
||||
def get_csr_data(self):
|
||||
"""Return bytes for self.csr."""
|
||||
return crypto.dump_certificate_request(crypto.FILETYPE_PEM, self.csr)
|
||||
|
||||
def _check_csr(self):
|
||||
def _check_subject(csr):
|
||||
subject = [(OpenSSL._util.lib.OBJ_txt2nid(to_bytes(sub[0])), to_bytes(sub[1])) for sub in self.subject]
|
||||
current_subject = [(OpenSSL._util.lib.OBJ_txt2nid(to_bytes(sub[0])), to_bytes(sub[1])) for sub in csr.get_subject().get_components()]
|
||||
if not set(subject) == set(current_subject):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _check_subjectAltName(extensions):
|
||||
altnames_ext = next((ext for ext in extensions if ext.get_short_name() == b'subjectAltName'), '')
|
||||
altnames = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
|
||||
to_text(altnames_ext, errors='surrogate_or_strict').split(',') if altname.strip()]
|
||||
if self.subjectAltName:
|
||||
if (set(altnames) != set([pyopenssl_normalize_name_attribute(to_text(name)) for name in self.subjectAltName]) or
|
||||
altnames_ext.get_critical() != self.subjectAltName_critical):
|
||||
return False
|
||||
else:
|
||||
if altnames:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _check_keyUsage_(extensions, extName, expected, critical):
|
||||
usages_ext = [ext for ext in extensions if ext.get_short_name() == extName]
|
||||
if (not usages_ext and expected) or (usages_ext and not expected):
|
||||
return False
|
||||
elif not usages_ext and not expected:
|
||||
return True
|
||||
else:
|
||||
current = [OpenSSL._util.lib.OBJ_txt2nid(to_bytes(usage.strip())) for usage in str(usages_ext[0]).split(',')]
|
||||
expected = [OpenSSL._util.lib.OBJ_txt2nid(to_bytes(usage)) for usage in expected]
|
||||
return set(current) == set(expected) and usages_ext[0].get_critical() == critical
|
||||
|
||||
def _check_keyUsage(extensions):
|
||||
usages_ext = [ext for ext in extensions if ext.get_short_name() == b'keyUsage']
|
||||
if (not usages_ext and self.keyUsage) or (usages_ext and not self.keyUsage):
|
||||
return False
|
||||
elif not usages_ext and not self.keyUsage:
|
||||
return True
|
||||
else:
|
||||
# OpenSSL._util.lib.OBJ_txt2nid() always returns 0 for all keyUsage values
|
||||
# (since keyUsage has a fixed bitfield for these values and is not extensible).
|
||||
# Therefore, we create an extension for the wanted values, and compare the
|
||||
# data of the extensions (which is the serialized bitfield).
|
||||
expected_ext = crypto.X509Extension(b"keyUsage", False, ', '.join(self.keyUsage).encode('ascii'))
|
||||
return usages_ext[0].get_data() == expected_ext.get_data() and usages_ext[0].get_critical() == self.keyUsage_critical
|
||||
|
||||
def _check_extenededKeyUsage(extensions):
|
||||
return _check_keyUsage_(extensions, b'extendedKeyUsage', self.extendedKeyUsage, self.extendedKeyUsage_critical)
|
||||
|
||||
def _check_basicConstraints(extensions):
|
||||
return _check_keyUsage_(extensions, b'basicConstraints', self.basicConstraints, self.basicConstraints_critical)
|
||||
|
||||
def _check_nameConstraints(extensions):
|
||||
nc_ext = next((ext for ext in extensions if ext.get_short_name() == b'nameConstraints'), '')
|
||||
permitted, excluded = pyopenssl_parse_name_constraints(nc_ext)
|
||||
if self.name_constraints_permitted or self.name_constraints_excluded:
|
||||
if set(permitted) != set([pyopenssl_normalize_name_attribute(to_text(name)) for name in self.name_constraints_permitted]):
|
||||
return False
|
||||
if set(excluded) != set([pyopenssl_normalize_name_attribute(to_text(name)) for name in self.name_constraints_excluded]):
|
||||
return False
|
||||
if nc_ext.get_critical() != self.name_constraints_critical:
|
||||
return False
|
||||
else:
|
||||
if permitted or excluded:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _check_ocspMustStaple(extensions):
|
||||
oms_ext = [ext for ext in extensions if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE]
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
|
||||
# Older versions of libssl don't know about OCSP Must Staple
|
||||
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
|
||||
if self.ocspMustStaple:
|
||||
return len(oms_ext) > 0 and oms_ext[0].get_critical() == self.ocspMustStaple_critical
|
||||
else:
|
||||
return len(oms_ext) == 0
|
||||
|
||||
def _check_extensions(csr):
|
||||
extensions = csr.get_extensions()
|
||||
return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and
|
||||
_check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and
|
||||
_check_ocspMustStaple(extensions) and _check_nameConstraints(extensions))
|
||||
|
||||
def _check_signature(csr):
|
||||
try:
|
||||
return csr.verify(self.privatekey)
|
||||
except crypto.Error:
|
||||
return False
|
||||
|
||||
return _check_subject(self.existing_csr) and _check_extensions(self.existing_csr) and _check_signature(self.existing_csr)
|
||||
|
||||
|
||||
def parse_crl_distribution_points(module, crl_distribution_points):
|
||||
result = []
|
||||
for index, parse_crl_distribution_point in enumerate(crl_distribution_points):
|
||||
@@ -592,9 +409,12 @@ 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)
|
||||
if self.ordered_subject:
|
||||
return subject == current_subject
|
||||
else:
|
||||
return set(subject) == set(current_subject)
|
||||
|
||||
def _find_extension(extensions, exttype):
|
||||
return next(
|
||||
@@ -604,8 +424,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 +498,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 +530,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)
|
||||
@@ -754,42 +574,20 @@ class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBack
|
||||
|
||||
|
||||
def select_backend(module, backend):
|
||||
if module.params['version'] != 1:
|
||||
module.deprecate('The version option will only support allowed values from community.crypto 2.0.0 on. '
|
||||
'Currently, only the value 1 is allowed by RFC 2986',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# First try cryptography, then pyOpenSSL
|
||||
# Try cryptography
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
try:
|
||||
getattr(crypto.X509Req, 'get_extensions')
|
||||
except AttributeError:
|
||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
|
||||
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, CertificateSigningRequestPyOpenSSLBackend(module)
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
@@ -805,8 +603,9 @@ def get_csr_argument_spec():
|
||||
privatekey_path=dict(type='path'),
|
||||
privatekey_content=dict(type='str', no_log=True),
|
||||
privatekey_passphrase=dict(type='str', no_log=True),
|
||||
version=dict(type='int', default=1),
|
||||
version=dict(type='int', default=1, choices=[1]),
|
||||
subject=dict(type='dict'),
|
||||
subject_ordered=dict(type='list', elements='dict'),
|
||||
country_name=dict(type='str', aliases=['C', 'countryName']),
|
||||
state_or_province_name=dict(type='str', aliases=['ST', 'stateOrProvinceName']),
|
||||
locality_name=dict(type='str', aliases=['L', 'localityName']),
|
||||
@@ -853,13 +652,14 @@ def get_csr_argument_spec():
|
||||
),
|
||||
mutually_exclusive=[('full_name', 'relative_name')]
|
||||
),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
|
||||
),
|
||||
required_together=[
|
||||
['authority_cert_issuer', 'authority_cert_serial_number'],
|
||||
],
|
||||
mutually_exclusive=[
|
||||
['privatekey_path', 'privatekey_content'],
|
||||
['subject', 'subject_ordered'],
|
||||
],
|
||||
required_one_of=[
|
||||
['privatekey_path', 'privatekey_content'],
|
||||
|
||||
@@ -17,7 +17,7 @@ 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.crypto.support import (
|
||||
load_certificate_request,
|
||||
@@ -30,38 +30,11 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
||||
cryptography_oid_to_name,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
|
||||
pyopenssl_get_extensions_from_csr,
|
||||
pyopenssl_normalize_name,
|
||||
pyopenssl_normalize_name_attribute,
|
||||
pyopenssl_parse_name_constraints,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||
get_publickey_info,
|
||||
)
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# OpenSSL 1.1.0 or newer
|
||||
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
|
||||
else:
|
||||
# OpenSSL 1.0.x or older
|
||||
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
|
||||
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
@@ -173,20 +146,19 @@ class CSRInfoRetrieval(object):
|
||||
'public_key_fingerprints': public_key_info['fingerprints'],
|
||||
})
|
||||
|
||||
if self.backend != 'pyopenssl':
|
||||
ski = self._get_subject_key_identifier()
|
||||
if ski is not None:
|
||||
ski = to_native(binascii.hexlify(ski))
|
||||
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
|
||||
result['subject_key_identifier'] = ski
|
||||
ski = self._get_subject_key_identifier()
|
||||
if ski is not None:
|
||||
ski = to_native(binascii.hexlify(ski))
|
||||
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
|
||||
result['subject_key_identifier'] = ski
|
||||
|
||||
aki, aci, acsn = self._get_authority_key_identifier()
|
||||
if aki is not None:
|
||||
aki = to_native(binascii.hexlify(aki))
|
||||
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
||||
result['authority_key_identifier'] = aki
|
||||
result['authority_cert_issuer'] = aci
|
||||
result['authority_cert_serial_number'] = acsn
|
||||
aki, aci, acsn = self._get_authority_key_identifier()
|
||||
if aki is not None:
|
||||
aki = to_native(binascii.hexlify(aki))
|
||||
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
||||
result['authority_key_identifier'] = aki
|
||||
result['authority_cert_issuer'] = aci
|
||||
result['authority_cert_serial_number'] = acsn
|
||||
|
||||
result['extensions_by_oid'] = self._get_all_extensions()
|
||||
|
||||
@@ -332,112 +304,9 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
|
||||
return self.csr.is_signature_valid
|
||||
|
||||
|
||||
class CSRInfoRetrievalPyOpenSSL(CSRInfoRetrieval):
|
||||
"""validate the supplied CSR."""
|
||||
|
||||
def __init__(self, module, content, validate_signature):
|
||||
super(CSRInfoRetrievalPyOpenSSL, self).__init__(module, 'pyopenssl', content, validate_signature)
|
||||
|
||||
def __get_name(self, name):
|
||||
result = []
|
||||
for sub in name.get_components():
|
||||
result.append([pyopenssl_normalize_name(sub[0]), to_text(sub[1])])
|
||||
return result
|
||||
|
||||
def _get_subject_ordered(self):
|
||||
return self.__get_name(self.csr.get_subject())
|
||||
|
||||
def _get_extension(self, short_name):
|
||||
for extension in self.csr.get_extensions():
|
||||
if extension.get_short_name() == short_name:
|
||||
result = [
|
||||
pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',')
|
||||
]
|
||||
return sorted(result), bool(extension.get_critical())
|
||||
return None, False
|
||||
|
||||
def _get_key_usage(self):
|
||||
return self._get_extension(b'keyUsage')
|
||||
|
||||
def _get_extended_key_usage(self):
|
||||
return self._get_extension(b'extendedKeyUsage')
|
||||
|
||||
def _get_basic_constraints(self):
|
||||
return self._get_extension(b'basicConstraints')
|
||||
|
||||
def _get_ocsp_must_staple(self):
|
||||
extensions = self.csr.get_extensions()
|
||||
oms_ext = [
|
||||
ext for ext in extensions
|
||||
if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE
|
||||
]
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
|
||||
# Older versions of libssl don't know about OCSP Must Staple
|
||||
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
|
||||
if oms_ext:
|
||||
return True, bool(oms_ext[0].get_critical())
|
||||
else:
|
||||
return None, False
|
||||
|
||||
def _get_subject_alt_name(self):
|
||||
for extension in self.csr.get_extensions():
|
||||
if extension.get_short_name() == b'subjectAltName':
|
||||
result = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
|
||||
to_text(extension, errors='surrogate_or_strict').split(', ')]
|
||||
return result, bool(extension.get_critical())
|
||||
return None, False
|
||||
|
||||
def _get_name_constraints(self):
|
||||
for extension in self.csr.get_extensions():
|
||||
if extension.get_short_name() == b'nameConstraints':
|
||||
permitted, excluded = pyopenssl_parse_name_constraints(extension)
|
||||
return permitted, excluded, bool(extension.get_critical())
|
||||
return None, None, False
|
||||
|
||||
def _get_public_key_pem(self):
|
||||
try:
|
||||
return crypto.dump_publickey(
|
||||
crypto.FILETYPE_PEM,
|
||||
self.csr.get_pubkey(),
|
||||
)
|
||||
except AttributeError:
|
||||
try:
|
||||
bio = crypto._new_mem_buf()
|
||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.csr.get_pubkey()._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
return crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
||||
|
||||
def _get_public_key_object(self):
|
||||
return self.csr.get_pubkey()
|
||||
|
||||
def _get_subject_key_identifier(self):
|
||||
# Won't be implemented
|
||||
return None
|
||||
|
||||
def _get_authority_key_identifier(self):
|
||||
# Won't be implemented
|
||||
return None, None, None
|
||||
|
||||
def _get_all_extensions(self):
|
||||
return pyopenssl_get_extensions_from_csr(self.csr)
|
||||
|
||||
def _is_signature_valid(self):
|
||||
try:
|
||||
return bool(self.csr.verify(self.csr.get_pubkey()))
|
||||
except crypto.Error:
|
||||
# OpenSSL error means that key is not consistent
|
||||
return False
|
||||
|
||||
|
||||
def get_csr_info(module, backend, content, validate_signature=True, prefer_one_fingerprint=False):
|
||||
if backend == 'cryptography':
|
||||
info = CSRInfoRetrievalCryptography(module, content, validate_signature=validate_signature)
|
||||
elif backend == 'pyopenssl':
|
||||
info = CSRInfoRetrievalPyOpenSSL(module, content, validate_signature=validate_signature)
|
||||
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
|
||||
|
||||
|
||||
@@ -445,34 +314,17 @@ def select_backend(module, backend, content, validate_signature=True):
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# First try cryptography, then pyOpenSSL
|
||||
# Try cryptography
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
module.fail_json(msg=("Can't detect the required Python library "
|
||||
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
try:
|
||||
getattr(crypto.X509Req, 'get_extensions')
|
||||
except AttributeError:
|
||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
|
||||
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, CSRInfoRetrievalPyOpenSSL(module, content, validate_signature=validate_signature)
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
|
||||
@@ -16,7 +16,7 @@ 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.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_X25519,
|
||||
@@ -46,20 +46,8 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
|
||||
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.6'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
@@ -263,61 +251,6 @@ class PrivateKeyBackend:
|
||||
return result
|
||||
|
||||
|
||||
# Implementation with using pyOpenSSL
|
||||
class PrivateKeyPyOpenSSLBackend(PrivateKeyBackend):
|
||||
|
||||
def __init__(self, module):
|
||||
super(PrivateKeyPyOpenSSLBackend, self).__init__(module=module, backend='pyopenssl')
|
||||
|
||||
if self.type == 'RSA':
|
||||
self.openssl_type = crypto.TYPE_RSA
|
||||
elif self.type == 'DSA':
|
||||
self.openssl_type = crypto.TYPE_DSA
|
||||
else:
|
||||
self.module.fail_json(msg="PyOpenSSL backend only supports RSA and DSA keys.")
|
||||
|
||||
if self.format != 'auto_ignore':
|
||||
self.module.fail_json(msg="PyOpenSSL backend only supports auto_ignore format.")
|
||||
|
||||
def generate_private_key(self):
|
||||
"""(Re-)Generate private key."""
|
||||
self.private_key = crypto.PKey()
|
||||
try:
|
||||
self.private_key.generate_key(self.openssl_type, self.size)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise PrivateKeyError(exc)
|
||||
|
||||
def _ensure_existing_private_key_loaded(self):
|
||||
if self.existing_private_key is None and self.has_existing():
|
||||
try:
|
||||
self.existing_private_key = load_privatekey(
|
||||
None, self.passphrase, content=self.existing_private_key_bytes, backend=self.backend)
|
||||
except OpenSSLBadPassphraseError as exc:
|
||||
raise PrivateKeyError(exc)
|
||||
|
||||
def get_private_key_data(self):
|
||||
"""Return bytes for self.private_key"""
|
||||
if self.cipher and self.passphrase:
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.private_key,
|
||||
self.cipher, to_bytes(self.passphrase))
|
||||
else:
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.private_key)
|
||||
|
||||
def _check_passphrase(self):
|
||||
try:
|
||||
load_privatekey(None, self.passphrase, content=self.existing_private_key_bytes, backend=self.backend)
|
||||
return True
|
||||
except Exception as dummy:
|
||||
return False
|
||||
|
||||
def _check_size_and_type(self):
|
||||
return self.size == self.existing_private_key.bits() and self.openssl_type == self.existing_private_key.type()
|
||||
|
||||
def _check_format(self):
|
||||
# Not supported by this backend
|
||||
return True
|
||||
|
||||
|
||||
# Implementation with using cryptography
|
||||
class PrivateKeyCryptographyBackend(PrivateKeyBackend):
|
||||
|
||||
@@ -550,36 +483,16 @@ def select_backend(module, backend):
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# Decision
|
||||
if module.params['cipher'] and module.params['passphrase'] and module.params['cipher'] != 'auto':
|
||||
# First try pyOpenSSL, then cryptography
|
||||
if can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
elif can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
else:
|
||||
# First try cryptography, then pyOpenSSL
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, PrivateKeyPyOpenSSLBackend(module)
|
||||
elif backend == 'cryptography':
|
||||
module.fail_json(msg=("Can't detect the required Python library "
|
||||
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||
if backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
@@ -605,7 +518,7 @@ def get_privatekey_argument_spec():
|
||||
cipher=dict(type='str'),
|
||||
format=dict(type='str', default='auto_ignore', choices=['pkcs1', 'pkcs8', 'raw', 'auto', 'auto_ignore']),
|
||||
format_mismatch=dict(type='str', default='regenerate', choices=['regenerate', 'convert']),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
|
||||
regenerate=dict(
|
||||
type='str',
|
||||
default='full_idempotence',
|
||||
|
||||
@@ -16,7 +16,7 @@ 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.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
@@ -36,24 +36,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.math impor
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||
_get_cryptography_public_key_info,
|
||||
_bigint_to_int,
|
||||
_get_pyopenssl_public_key_info,
|
||||
)
|
||||
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
@@ -69,20 +55,21 @@ else:
|
||||
SIGNATURE_TEST_DATA = b'1234'
|
||||
|
||||
|
||||
def _get_cryptography_private_key_info(key):
|
||||
def _get_cryptography_private_key_info(key, need_private_key_data=False):
|
||||
key_type, key_public_data = _get_cryptography_public_key_info(key.public_key())
|
||||
key_private_data = dict()
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
private_numbers = key.private_numbers()
|
||||
key_private_data['p'] = private_numbers.p
|
||||
key_private_data['q'] = private_numbers.q
|
||||
key_private_data['exponent'] = private_numbers.d
|
||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
||||
private_numbers = key.private_numbers()
|
||||
key_private_data['x'] = private_numbers.x
|
||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
private_numbers = key.private_numbers()
|
||||
key_private_data['multiplier'] = private_numbers.private_value
|
||||
if need_private_key_data:
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
private_numbers = key.private_numbers()
|
||||
key_private_data['p'] = private_numbers.p
|
||||
key_private_data['q'] = private_numbers.q
|
||||
key_private_data['exponent'] = private_numbers.d
|
||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
||||
private_numbers = key.private_numbers()
|
||||
key_private_data['x'] = private_numbers.x
|
||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
private_numbers = key.private_numbers()
|
||||
key_private_data['multiplier'] = private_numbers.private_value
|
||||
return key_type, key_public_data, key_private_data
|
||||
|
||||
|
||||
@@ -188,20 +175,21 @@ class PrivateKeyParseError(OpenSSLObjectError):
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class PrivateKeyInfoRetrieval(object):
|
||||
def __init__(self, module, backend, content, passphrase=None, return_private_key_data=False):
|
||||
def __init__(self, module, backend, content, passphrase=None, return_private_key_data=False, check_consistency=False):
|
||||
# content must be a bytes string
|
||||
self.module = module
|
||||
self.backend = backend
|
||||
self.content = content
|
||||
self.passphrase = passphrase
|
||||
self.return_private_key_data = return_private_key_data
|
||||
self.check_consistency = check_consistency
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key(self, binary):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_key_info(self):
|
||||
def _get_key_info(self, need_private_key_data=False):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -230,20 +218,22 @@ class PrivateKeyInfoRetrieval(object):
|
||||
result['public_key_fingerprints'] = get_fingerprint_of_bytes(
|
||||
pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict()
|
||||
|
||||
key_type, key_public_data, key_private_data = self._get_key_info()
|
||||
key_type, key_public_data, key_private_data = self._get_key_info(
|
||||
need_private_key_data=self.return_private_key_data or self.check_consistency)
|
||||
result['type'] = key_type
|
||||
result['public_data'] = key_public_data
|
||||
if self.return_private_key_data:
|
||||
result['private_data'] = key_private_data
|
||||
|
||||
result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data)
|
||||
if result['key_is_consistent'] is False:
|
||||
# Only fail when it is False, to avoid to fail on None (which means "we don't know")
|
||||
msg = (
|
||||
"Private key is not consistent! (See "
|
||||
"https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)"
|
||||
)
|
||||
raise PrivateKeyConsistencyError(msg, result)
|
||||
if self.check_consistency:
|
||||
result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data)
|
||||
if result['key_is_consistent'] is False:
|
||||
# Only fail when it is False, to avoid to fail on None (which means "we don't know")
|
||||
msg = (
|
||||
"Private key is not consistent! (See "
|
||||
"https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)"
|
||||
)
|
||||
raise PrivateKeyConsistencyError(msg, result)
|
||||
return result
|
||||
|
||||
|
||||
@@ -258,177 +248,39 @@ class PrivateKeyInfoRetrievalCryptography(PrivateKeyInfoRetrieval):
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
def _get_key_info(self):
|
||||
return _get_cryptography_private_key_info(self.key)
|
||||
def _get_key_info(self, need_private_key_data=False):
|
||||
return _get_cryptography_private_key_info(self.key, need_private_key_data=need_private_key_data)
|
||||
|
||||
def _is_key_consistent(self, key_public_data, key_private_data):
|
||||
return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data)
|
||||
|
||||
|
||||
class PrivateKeyInfoRetrievalPyOpenSSL(PrivateKeyInfoRetrieval):
|
||||
"""validate the supplied private key."""
|
||||
|
||||
def __init__(self, module, content, **kwargs):
|
||||
super(PrivateKeyInfoRetrievalPyOpenSSL, self).__init__(module, 'pyopenssl', content, **kwargs)
|
||||
|
||||
def _get_public_key(self, binary):
|
||||
try:
|
||||
return crypto.dump_publickey(
|
||||
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
|
||||
self.key
|
||||
)
|
||||
except AttributeError:
|
||||
try:
|
||||
# pyOpenSSL < 16.0:
|
||||
bio = crypto._new_mem_buf()
|
||||
if binary:
|
||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.key._pkey)
|
||||
else:
|
||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.key._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
return crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
||||
|
||||
def _get_key_info(self):
|
||||
key_type, key_public_data, try_fallback = _get_pyopenssl_public_key_info(self.key)
|
||||
key_private_data = dict()
|
||||
openssl_key_type = self.key.type()
|
||||
if crypto.TYPE_RSA == openssl_key_type:
|
||||
try:
|
||||
# Use OpenSSL directly to extract key data
|
||||
key = OpenSSL._util.lib.EVP_PKEY_get1_RSA(self.key._pkey)
|
||||
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.RSA_free)
|
||||
# OpenSSL 1.1 and newer have functions to extract the parameters
|
||||
# from the EVP PKEY data structures. Older versions didn't have
|
||||
# these getters, and it was common use to simply access the values
|
||||
# directly. Since there's no guarantee that these data structures
|
||||
# will still be accessible in the future, we use the getters for
|
||||
# 1.1 and later, and directly access the values for 1.0.x and
|
||||
# earlier.
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# Get modulus and exponents
|
||||
n = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
e = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
d = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.RSA_get0_key(key, n, e, d)
|
||||
key_private_data['exponent'] = _bigint_to_int(d[0])
|
||||
# Get factors
|
||||
p = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
q = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.RSA_get0_factors(key, p, q)
|
||||
key_private_data['p'] = _bigint_to_int(p[0])
|
||||
key_private_data['q'] = _bigint_to_int(q[0])
|
||||
else:
|
||||
# Get private exponent
|
||||
key_private_data['exponent'] = _bigint_to_int(key.d)
|
||||
# Get factors
|
||||
key_private_data['p'] = _bigint_to_int(key.p)
|
||||
key_private_data['q'] = _bigint_to_int(key.q)
|
||||
except AttributeError:
|
||||
try_fallback = True
|
||||
elif crypto.TYPE_DSA == openssl_key_type:
|
||||
try:
|
||||
# Use OpenSSL directly to extract key data
|
||||
key = OpenSSL._util.lib.EVP_PKEY_get1_DSA(self.key._pkey)
|
||||
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.DSA_free)
|
||||
# OpenSSL 1.1 and newer have functions to extract the parameters
|
||||
# from the EVP PKEY data structures. Older versions didn't have
|
||||
# these getters, and it was common use to simply access the values
|
||||
# directly. Since there's no guarantee that these data structures
|
||||
# will still be accessible in the future, we use the getters for
|
||||
# 1.1 and later, and directly access the values for 1.0.x and
|
||||
# earlier.
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# Get private key exponents
|
||||
y = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
x = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.DSA_get0_key(key, y, x)
|
||||
key_private_data['x'] = _bigint_to_int(x[0])
|
||||
else:
|
||||
# Get private key exponents
|
||||
key_private_data['x'] = _bigint_to_int(key.priv_key)
|
||||
except AttributeError:
|
||||
try_fallback = True
|
||||
else:
|
||||
# Return 'unknown'
|
||||
key_type = 'unknown ({0})'.format(self.key.type())
|
||||
# If needed and if possible, fall back to cryptography
|
||||
if try_fallback and PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
|
||||
return _get_cryptography_private_key_info(self.key.to_cryptography_key())
|
||||
return key_type, key_public_data, key_private_data
|
||||
|
||||
def _is_key_consistent(self, key_public_data, key_private_data):
|
||||
openssl_key_type = self.key.type()
|
||||
if crypto.TYPE_RSA == openssl_key_type:
|
||||
try:
|
||||
return self.key.check()
|
||||
except crypto.Error:
|
||||
# OpenSSL error means that key is not consistent
|
||||
return False
|
||||
if crypto.TYPE_DSA == openssl_key_type:
|
||||
result = _check_dsa_consistency(key_public_data, key_private_data)
|
||||
if result is not None:
|
||||
return result
|
||||
signature = crypto.sign(self.key, SIGNATURE_TEST_DATA, 'sha256')
|
||||
# Verify wants a cert (where it can get the public key from)
|
||||
cert = crypto.X509()
|
||||
cert.set_pubkey(self.key)
|
||||
try:
|
||||
crypto.verify(cert, signature, SIGNATURE_TEST_DATA, 'sha256')
|
||||
return True
|
||||
except crypto.Error:
|
||||
return False
|
||||
# If needed and if possible, fall back to cryptography
|
||||
if PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
|
||||
return _is_cryptography_key_consistent(self.key.to_cryptography_key(), key_public_data, key_private_data)
|
||||
return None
|
||||
|
||||
|
||||
def get_privatekey_info(module, backend, content, passphrase=None, return_private_key_data=False, prefer_one_fingerprint=False):
|
||||
if backend == 'cryptography':
|
||||
info = PrivateKeyInfoRetrievalCryptography(
|
||||
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
|
||||
elif backend == 'pyopenssl':
|
||||
info = PrivateKeyInfoRetrievalPyOpenSSL(
|
||||
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
|
||||
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
|
||||
|
||||
|
||||
def select_backend(module, backend, content, passphrase=None, return_private_key_data=False):
|
||||
def select_backend(module, backend, content, passphrase=None, return_private_key_data=False, check_consistency=False):
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# First try cryptography, then pyOpenSSL
|
||||
# Try cryptography
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
module.fail_json(msg=("Can't detect the required Python library "
|
||||
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, PrivateKeyInfoRetrievalPyOpenSSL(
|
||||
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
return backend, PrivateKeyInfoRetrievalCryptography(
|
||||
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
|
||||
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data, check_consistency=check_consistency)
|
||||
else:
|
||||
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||
|
||||
@@ -14,7 +14,7 @@ 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.crypto.basic import (
|
||||
CRYPTOGRAPHY_HAS_X25519,
|
||||
@@ -31,18 +31,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
||||
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||
MINIMAL_PYOPENSSL_VERSION = '16.0.0' # when working with public key objects, the minimal required version is 0.15
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
@@ -93,98 +81,6 @@ def _get_cryptography_public_key_info(key):
|
||||
return key_type, key_public_data
|
||||
|
||||
|
||||
def _bigint_to_int(bn):
|
||||
'''Convert OpenSSL BIGINT to Python integer'''
|
||||
if bn == OpenSSL._util.ffi.NULL:
|
||||
return None
|
||||
hexstr = OpenSSL._util.lib.BN_bn2hex(bn)
|
||||
try:
|
||||
return int(OpenSSL._util.ffi.string(hexstr), 16)
|
||||
finally:
|
||||
OpenSSL._util.lib.OPENSSL_free(hexstr)
|
||||
|
||||
|
||||
def _get_pyopenssl_public_key_info(key):
|
||||
key_public_data = dict()
|
||||
try_fallback = True
|
||||
openssl_key_type = key.type()
|
||||
if crypto.TYPE_RSA == openssl_key_type:
|
||||
key_type = 'RSA'
|
||||
key_public_data['size'] = key.bits()
|
||||
|
||||
try:
|
||||
# Use OpenSSL directly to extract key data
|
||||
key = OpenSSL._util.lib.EVP_PKEY_get1_RSA(key._pkey)
|
||||
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.RSA_free)
|
||||
# OpenSSL 1.1 and newer have functions to extract the parameters
|
||||
# from the EVP PKEY data structures. Older versions didn't have
|
||||
# these getters, and it was common use to simply access the values
|
||||
# directly. Since there's no guarantee that these data structures
|
||||
# will still be accessible in the future, we use the getters for
|
||||
# 1.1 and later, and directly access the values for 1.0.x and
|
||||
# earlier.
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# Get modulus and exponents
|
||||
n = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
e = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
d = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.RSA_get0_key(key, n, e, d)
|
||||
key_public_data['modulus'] = _bigint_to_int(n[0])
|
||||
key_public_data['exponent'] = _bigint_to_int(e[0])
|
||||
else:
|
||||
# Get modulus and exponents
|
||||
key_public_data['modulus'] = _bigint_to_int(key.n)
|
||||
key_public_data['exponent'] = _bigint_to_int(key.e)
|
||||
try_fallback = False
|
||||
except AttributeError:
|
||||
# Use fallback if available
|
||||
pass
|
||||
elif crypto.TYPE_DSA == openssl_key_type:
|
||||
key_type = 'DSA'
|
||||
key_public_data['size'] = key.bits()
|
||||
|
||||
try:
|
||||
# Use OpenSSL directly to extract key data
|
||||
key = OpenSSL._util.lib.EVP_PKEY_get1_DSA(key._pkey)
|
||||
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.DSA_free)
|
||||
# OpenSSL 1.1 and newer have functions to extract the parameters
|
||||
# from the EVP PKEY data structures. Older versions didn't have
|
||||
# these getters, and it was common use to simply access the values
|
||||
# directly. Since there's no guarantee that these data structures
|
||||
# will still be accessible in the future, we use the getters for
|
||||
# 1.1 and later, and directly access the values for 1.0.x and
|
||||
# earlier.
|
||||
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
|
||||
# Get public parameters (primes and group element)
|
||||
p = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
q = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
g = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.DSA_get0_pqg(key, p, q, g)
|
||||
key_public_data['p'] = _bigint_to_int(p[0])
|
||||
key_public_data['q'] = _bigint_to_int(q[0])
|
||||
key_public_data['g'] = _bigint_to_int(g[0])
|
||||
# Get public key exponents
|
||||
y = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
x = OpenSSL._util.ffi.new("BIGNUM **")
|
||||
OpenSSL._util.lib.DSA_get0_key(key, y, x)
|
||||
key_public_data['y'] = _bigint_to_int(y[0])
|
||||
else:
|
||||
# Get public parameters (primes and group element)
|
||||
key_public_data['p'] = _bigint_to_int(key.p)
|
||||
key_public_data['q'] = _bigint_to_int(key.q)
|
||||
key_public_data['g'] = _bigint_to_int(key.g)
|
||||
# Get public key exponents
|
||||
key_public_data['y'] = _bigint_to_int(key.pub_key)
|
||||
try_fallback = False
|
||||
except AttributeError:
|
||||
# Use fallback if available
|
||||
pass
|
||||
else:
|
||||
# Return 'unknown'
|
||||
key_type = 'unknown ({0})'.format(key.type())
|
||||
return key_type, key_public_data, try_fallback
|
||||
|
||||
|
||||
class PublicKeyParseError(OpenSSLObjectError):
|
||||
def __init__(self, msg, result):
|
||||
super(PublicKeyParseError, self).__init__(msg)
|
||||
@@ -242,46 +138,9 @@ class PublicKeyInfoRetrievalCryptography(PublicKeyInfoRetrieval):
|
||||
return _get_cryptography_public_key_info(self.key)
|
||||
|
||||
|
||||
class PublicKeyInfoRetrievalPyOpenSSL(PublicKeyInfoRetrieval):
|
||||
"""validate the supplied public key."""
|
||||
|
||||
def __init__(self, module, content=None, key=None):
|
||||
super(PublicKeyInfoRetrievalPyOpenSSL, self).__init__(module, 'pyopenssl', content=content, key=key)
|
||||
|
||||
def _get_public_key(self, binary):
|
||||
try:
|
||||
return crypto.dump_publickey(
|
||||
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
|
||||
self.key
|
||||
)
|
||||
except AttributeError:
|
||||
try:
|
||||
# pyOpenSSL < 16.0:
|
||||
bio = crypto._new_mem_buf()
|
||||
if binary:
|
||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.key._pkey)
|
||||
else:
|
||||
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.key._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
return crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
|
||||
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
|
||||
|
||||
def _get_key_info(self):
|
||||
key_type, key_public_data, try_fallback = _get_pyopenssl_public_key_info(self.key)
|
||||
# If needed and if possible, fall back to cryptography
|
||||
if try_fallback and PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
|
||||
return _get_cryptography_public_key_info(self.key.to_cryptography_key())
|
||||
return key_type, key_public_data
|
||||
|
||||
|
||||
def get_publickey_info(module, backend, content=None, key=None, prefer_one_fingerprint=False):
|
||||
if backend == 'cryptography':
|
||||
info = PublicKeyInfoRetrievalCryptography(module, content=content, key=key)
|
||||
elif backend == 'pyopenssl':
|
||||
info = PublicKeyInfoRetrievalPyOpenSSL(module, content=content, key=key)
|
||||
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
|
||||
|
||||
|
||||
@@ -289,29 +148,17 @@ def select_backend(module, backend, content=None, key=None):
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# First try cryptography, then pyOpenSSL
|
||||
# Try cryptography
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
return backend, PublicKeyInfoRetrievalPyOpenSSL(module, content=content, key=key)
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2019, Felix Fontein <felix@fontein.de>
|
||||
#
|
||||
# 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 base64
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
||||
|
||||
try:
|
||||
import OpenSSL
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
pass
|
||||
|
||||
from ._objects import (
|
||||
NORMALIZE_NAMES_SHORT,
|
||||
NORMALIZE_NAMES,
|
||||
)
|
||||
|
||||
from ._obj2txt import obj2txt
|
||||
|
||||
from .basic import (
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
|
||||
def pyopenssl_normalize_name(name, short=False):
|
||||
nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(name))
|
||||
if nid != 0:
|
||||
b_name = OpenSSL._util.lib.OBJ_nid2ln(nid)
|
||||
name = to_text(OpenSSL._util.ffi.string(b_name))
|
||||
if short:
|
||||
return NORMALIZE_NAMES_SHORT.get(name, name)
|
||||
else:
|
||||
return NORMALIZE_NAMES.get(name, name)
|
||||
|
||||
|
||||
def pyopenssl_normalize_name_attribute(san):
|
||||
# apparently openssl returns 'IP address' not 'IP' as specifier when converting the subjectAltName to string
|
||||
# although it won't accept this specifier when generating the CSR. (https://github.com/openssl/openssl/issues/4004)
|
||||
if san.startswith('IP Address:'):
|
||||
san = 'IP:' + san[len('IP Address:'):]
|
||||
if san.startswith('IP:'):
|
||||
address = san[3:]
|
||||
if '/' in address:
|
||||
ip = compat_ipaddress.ip_network(address)
|
||||
san = 'IP:{0}/{1}'.format(ip.network_address.compressed, ip.prefixlen)
|
||||
else:
|
||||
ip = compat_ipaddress.ip_address(address)
|
||||
san = 'IP:{0}'.format(ip.compressed)
|
||||
if san.startswith('Registered ID:'):
|
||||
san = 'RID:' + san[len('Registered ID:'):]
|
||||
# Some versions of OpenSSL apparently forgot the colon. Happens in CI with Ubuntu 16.04 and FreeBSD 11.1
|
||||
if san.startswith('Registered ID'):
|
||||
san = 'RID:' + san[len('Registered ID'):]
|
||||
return san
|
||||
|
||||
|
||||
def pyopenssl_get_extensions_from_cert(cert):
|
||||
# While pyOpenSSL allows us to get an extension's DER value, it won't
|
||||
# give us the dotted string for an OID. So we have to do some magic to
|
||||
# get hold of it.
|
||||
result = dict()
|
||||
ext_count = cert.get_extension_count()
|
||||
for i in range(0, ext_count):
|
||||
ext = cert.get_extension(i)
|
||||
entry = dict(
|
||||
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.
|
||||
result[oid] = entry
|
||||
return result
|
||||
|
||||
|
||||
def pyopenssl_get_extensions_from_csr(csr):
|
||||
# While pyOpenSSL allows us to get an extension's DER value, it won't
|
||||
# give us the dotted string for an OID. So we have to do some magic to
|
||||
# get hold of it.
|
||||
result = dict()
|
||||
for ext in csr.get_extensions():
|
||||
entry = dict(
|
||||
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.
|
||||
result[oid] = entry
|
||||
return result
|
||||
|
||||
|
||||
def pyopenssl_parse_name_constraints(name_constraints_extension):
|
||||
lines = to_text(name_constraints_extension, errors='surrogate_or_strict').splitlines()
|
||||
exclude = None
|
||||
excluded = []
|
||||
permitted = []
|
||||
for line in lines:
|
||||
if line.startswith(' ') or line.startswith('\t'):
|
||||
name = pyopenssl_normalize_name_attribute(line.strip())
|
||||
if exclude is True:
|
||||
excluded.append(name)
|
||||
elif exclude is False:
|
||||
permitted.append(name)
|
||||
else:
|
||||
raise OpenSSLObjectError('Unexpected nameConstraint line: "{0}"'.format(line))
|
||||
else:
|
||||
line_lc = line.lower()
|
||||
if line_lc.startswith('exclud'):
|
||||
exclude = True
|
||||
elif line_lc.startswith('includ') or line_lc.startswith('permitt'):
|
||||
exclude = False
|
||||
else:
|
||||
raise OpenSSLObjectError('Cannot parse nameConstraint line: "{0}"'.format(line))
|
||||
return permitted, excluded
|
||||
@@ -27,7 +27,7 @@ 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
|
||||
@@ -98,25 +98,10 @@ def get_fingerprint_of_bytes(source, prefer_one=False):
|
||||
return fingerprint
|
||||
|
||||
|
||||
def get_fingerprint_of_privatekey(privatekey, backend='pyopenssl', prefer_one=False):
|
||||
def get_fingerprint_of_privatekey(privatekey, backend='cryptography', prefer_one=False):
|
||||
"""Generate the fingerprint of the public key. """
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
try:
|
||||
publickey = crypto.dump_publickey(crypto.FILETYPE_ASN1, privatekey)
|
||||
except AttributeError:
|
||||
# If PyOpenSSL < 16.0 crypto.dump_publickey() will fail.
|
||||
try:
|
||||
bio = crypto._new_mem_buf()
|
||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, privatekey._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
publickey = crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
# By doing this we prevent the code from raising an error
|
||||
# yet we return no value in the fingerprint hash.
|
||||
return None
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
publickey = privatekey.public_key().public_bytes(
|
||||
serialization.Encoding.DER,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
@@ -125,7 +110,7 @@ def get_fingerprint_of_privatekey(privatekey, backend='pyopenssl', prefer_one=Fa
|
||||
return get_fingerprint_of_bytes(publickey, prefer_one=prefer_one)
|
||||
|
||||
|
||||
def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl', prefer_one=False):
|
||||
def get_fingerprint(path, passphrase=None, content=None, backend='cryptography', prefer_one=False):
|
||||
"""Generate the fingerprint of the public key. """
|
||||
|
||||
privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend)
|
||||
@@ -133,7 +118,7 @@ def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl', pr
|
||||
return get_fingerprint_of_privatekey(privatekey, backend=backend, prefer_one=prefer_one)
|
||||
|
||||
|
||||
def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='pyopenssl'):
|
||||
def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='cryptography'):
|
||||
"""Load the specified OpenSSL private key.
|
||||
|
||||
The content can also be specified via content; in that case,
|
||||
@@ -213,14 +198,9 @@ def load_publickey(path=None, content=None, backend=None):
|
||||
return serialization.load_pem_public_key(content, backend=cryptography_backend())
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e))
|
||||
else:
|
||||
try:
|
||||
return crypto.load_publickey(crypto.FILETYPE_PEM, content)
|
||||
except crypto.Error as e:
|
||||
raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e))
|
||||
|
||||
|
||||
def load_certificate(path, content=None, backend='pyopenssl'):
|
||||
def load_certificate(path, content=None, backend='cryptography'):
|
||||
"""Load the specified certificate."""
|
||||
|
||||
try:
|
||||
@@ -240,7 +220,7 @@ def load_certificate(path, content=None, backend='pyopenssl'):
|
||||
raise OpenSSLObjectError(exc)
|
||||
|
||||
|
||||
def load_certificate_request(path, content=None, backend='pyopenssl'):
|
||||
def load_certificate_request(path, content=None, backend='cryptography'):
|
||||
"""Load the specified certificate signing request."""
|
||||
try:
|
||||
if content is None:
|
||||
@@ -250,25 +230,50 @@ def load_certificate_request(path, content=None, backend='pyopenssl'):
|
||||
csr_content = content
|
||||
except (IOError, OSError) as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
if backend == 'pyopenssl':
|
||||
return crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_content)
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
try:
|
||||
return x509.load_pem_x509_csr(csr_content, cryptography_backend())
|
||||
except ValueError as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
|
||||
|
||||
def parse_name_field(input_dict):
|
||||
def parse_name_field(input_dict, name_field_name=None):
|
||||
"""Take a dict with key: value or key: list_of_values mappings and return a list of tuples"""
|
||||
error_str = '{key}' if name_field_name is None else '{key} in {name}'
|
||||
|
||||
result = []
|
||||
for key, value in input_dict.items():
|
||||
if isinstance(value, list):
|
||||
for entry in value:
|
||||
if not isinstance(entry, six.string_types):
|
||||
raise TypeError(('Values %s must be strings' % error_str).format(key=key, name=name_field_name))
|
||||
if not entry:
|
||||
raise ValueError(('Values for %s must not be empty strings' % error_str).format(key=key))
|
||||
result.append((key, entry))
|
||||
elif isinstance(value, six.string_types):
|
||||
if not value:
|
||||
raise ValueError(('Value for %s must not be an empty string' % error_str).format(key=key))
|
||||
result.append((key, value))
|
||||
else:
|
||||
raise TypeError(('Value for %s must be either a string or a list of strings' % error_str).format(key=key))
|
||||
return result
|
||||
|
||||
|
||||
def parse_ordered_name_field(input_list, name_field_name):
|
||||
"""Take a dict with key: value or key: list_of_values mappings and return a list of tuples"""
|
||||
|
||||
result = []
|
||||
for key in input_dict:
|
||||
if isinstance(input_dict[key], list):
|
||||
for entry in input_dict[key]:
|
||||
result.append((key, entry))
|
||||
else:
|
||||
result.append((key, input_dict[key]))
|
||||
for index, entry in enumerate(input_list):
|
||||
if len(entry) != 1:
|
||||
raise ValueError(
|
||||
'Entry #{index} in {name} must be a dictionary with exactly one key-value pair'.format(
|
||||
name=name_field_name, index=index + 1))
|
||||
try:
|
||||
result.extend(parse_name_field(entry, name_field_name=name_field_name))
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError(
|
||||
'Error while processing entry #{index} in {name}: {error}'.format(
|
||||
name=name_field_name, index=index + 1, error=exc))
|
||||
return result
|
||||
|
||||
|
||||
@@ -322,9 +327,7 @@ def get_relative_time_option(input_string, input_name, backend='cryptography'):
|
||||
elif backend == 'cryptography':
|
||||
return result_datetime
|
||||
# Absolute time
|
||||
if backend == 'pyopenssl':
|
||||
return input_string
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
for date_fmt in ['%Y%m%d%H%M%SZ', '%Y%m%d%H%MZ', '%Y%m%d%H%M%S%z', '%Y%m%d%H%M%z']:
|
||||
try:
|
||||
return datetime.datetime.strptime(result, date_fmt)
|
||||
|
||||
@@ -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
|
||||
|
||||
328
plugins/module_utils/openssh/backends/common.py
Normal file
328
plugins/module_utils/openssh/backends/common.py
Normal file
@@ -0,0 +1,328 @@
|
||||
# -*- 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
|
||||
|
||||
from ansible.module_utils import six
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
|
||||
parse_openssh_version,
|
||||
)
|
||||
|
||||
|
||||
def restore_on_failure(f):
|
||||
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):
|
||||
self._execute()
|
||||
self.module.exit_json(**self.result)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _execute(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
result = self._result
|
||||
|
||||
result['changed'] = self.changed
|
||||
|
||||
if self.module._diff:
|
||||
result['diff'] = self.diff
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def _result(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def diff(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def skip_if_check_mode(f):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if not self.check_mode:
|
||||
f(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
@staticmethod
|
||||
def trigger_change(f):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
f(self, *args, **kwargs)
|
||||
self.changed = True
|
||||
return wrapper
|
||||
|
||||
def _check_if_base_dir(self, path):
|
||||
base_dir = os.path.dirname(path) or '.'
|
||||
if not os.path.isdir(base_dir):
|
||||
self.module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
|
||||
def _get_ssh_version(self):
|
||||
ssh_bin = self.module.get_bin_path('ssh')
|
||||
if not ssh_bin:
|
||||
return ""
|
||||
return parse_openssh_version(self.module.run_command([ssh_bin, '-V', '-q'])[2].strip())
|
||||
|
||||
@_restore_all_on_failure
|
||||
def _safe_secure_move(self, sources_and_destinations):
|
||||
"""Moves a list of files from 'source' to 'destination' and restores 'destination' from backup upon failure.
|
||||
If 'destination' does not already exist, then 'source' permissions are preserved to prevent
|
||||
exposing protected data ('atomic_move' uses the 'destination' base directory mask for
|
||||
permissions if 'destination' does not already exists).
|
||||
"""
|
||||
for source, destination in sources_and_destinations:
|
||||
if os.path.exists(destination):
|
||||
self.module.atomic_move(source, destination)
|
||||
else:
|
||||
self.module.preserved_copy(source, destination)
|
||||
|
||||
def _update_permissions(self, path):
|
||||
file_args = self.module.load_file_common_arguments(self.module.params)
|
||||
file_args['path'] = path
|
||||
|
||||
if not self.module.check_file_absent_if_check_mode(path):
|
||||
self.changed = self.module.set_fs_attributes_if_different(file_args, self.changed)
|
||||
else:
|
||||
self.changed = True
|
||||
|
||||
|
||||
class KeygenCommand(object):
|
||||
def __init__(self, module):
|
||||
self._bin_path = module.get_bin_path('ssh-keygen', True)
|
||||
self._run_command = module.run_command
|
||||
|
||||
def generate_certificate(self, certificate_path, identifier, options, pkcs11_provider, principals,
|
||||
serial_number, signature_algorithm, signing_key_path, type,
|
||||
time_parameters, use_agent, **kwargs):
|
||||
args = [self._bin_path, '-s', signing_key_path, '-P', '', '-I', identifier]
|
||||
|
||||
if options:
|
||||
for option in options:
|
||||
args.extend(['-O', option])
|
||||
if pkcs11_provider:
|
||||
args.extend(['-D', pkcs11_provider])
|
||||
if principals:
|
||||
args.extend(['-n', ','.join(principals)])
|
||||
if serial_number is not None:
|
||||
args.extend(['-z', str(serial_number)])
|
||||
if type == 'host':
|
||||
args.extend(['-h'])
|
||||
if use_agent:
|
||||
args.extend(['-U'])
|
||||
if time_parameters.validity_string:
|
||||
args.extend(['-V', time_parameters.validity_string])
|
||||
if signature_algorithm:
|
||||
args.extend(['-t', signature_algorithm])
|
||||
args.append(certificate_path)
|
||||
|
||||
return self._run_command(args, **kwargs)
|
||||
|
||||
def generate_keypair(self, private_key_path, size, type, comment, **kwargs):
|
||||
args = [
|
||||
self._bin_path,
|
||||
'-q',
|
||||
'-N', '',
|
||||
'-b', str(size),
|
||||
'-t', type,
|
||||
'-f', private_key_path,
|
||||
'-C', comment or ''
|
||||
]
|
||||
|
||||
# "y" must be entered in response to the "overwrite" prompt
|
||||
data = 'y' if os.path.exists(private_key_path) else None
|
||||
|
||||
return self._run_command(args, data=data, **kwargs)
|
||||
|
||||
def get_certificate_info(self, certificate_path, **kwargs):
|
||||
return self._run_command([self._bin_path, '-L', '-f', certificate_path], **kwargs)
|
||||
|
||||
def get_matching_public_key(self, private_key_path, **kwargs):
|
||||
return self._run_command([self._bin_path, '-P', '', '-y', '-f', private_key_path], **kwargs)
|
||||
|
||||
def get_private_key(self, private_key_path, **kwargs):
|
||||
return self._run_command([self._bin_path, '-l', '-f', private_key_path], **kwargs)
|
||||
|
||||
def update_comment(self, private_key_path, comment, **kwargs):
|
||||
if os.path.exists(private_key_path) and not os.access(private_key_path, os.W_OK):
|
||||
try:
|
||||
os.chmod(private_key_path, stat.S_IWUSR + stat.S_IRUSR)
|
||||
except (IOError, OSError) as e:
|
||||
raise e("The private key at %s is not writeable preventing a comment update" % private_key_path)
|
||||
|
||||
return self._run_command([self._bin_path, '-q', '-o', '-c', '-C', comment, '-f', private_key_path], **kwargs)
|
||||
|
||||
|
||||
class PrivateKey(object):
|
||||
def __init__(self, size, key_type, fingerprint):
|
||||
self._size = size
|
||||
self._type = key_type
|
||||
self._fingerprint = fingerprint
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def fingerprint(self):
|
||||
return self._fingerprint
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, string):
|
||||
properties = string.split()
|
||||
|
||||
return cls(
|
||||
size=int(properties[0]),
|
||||
key_type=properties[-1][1:-1].lower(),
|
||||
fingerprint=properties[1],
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'size': self._size,
|
||||
'type': self._type,
|
||||
'fingerprint': self._fingerprint,
|
||||
}
|
||||
|
||||
|
||||
class PublicKey(object):
|
||||
def __init__(self, type_string, data, comment):
|
||||
self._type_string = type_string
|
||||
self._data = data
|
||||
self._comment = comment
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, type(self)):
|
||||
return NotImplemented
|
||||
|
||||
return all([
|
||||
self._type_string == other._type_string,
|
||||
self._data == other._data,
|
||||
(self._comment == other._comment) if self._comment is not None and other._comment is not None else True
|
||||
])
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s" % (self._type_string, self._data)
|
||||
|
||||
@property
|
||||
def comment(self):
|
||||
return self._comment
|
||||
|
||||
@comment.setter
|
||||
def comment(self, value):
|
||||
self._comment = value
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def type_string(self):
|
||||
return self._type_string
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, string):
|
||||
properties = string.strip('\n').split(' ', 2)
|
||||
|
||||
return cls(
|
||||
type_string=properties[0],
|
||||
data=properties[1],
|
||||
comment=properties[2] if len(properties) > 2 else ""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load(cls, path):
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
properties = f.read().strip(' \n').split(' ', 2)
|
||||
except (IOError, OSError):
|
||||
raise
|
||||
|
||||
if len(properties) < 2:
|
||||
return None
|
||||
|
||||
return cls(
|
||||
type_string=properties[0],
|
||||
data=properties[1],
|
||||
comment='' if len(properties) <= 2 else properties[2],
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'comment': self._comment,
|
||||
'public_key': self._data,
|
||||
}
|
||||
@@ -20,16 +20,13 @@ 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.openssh.cryptography import (
|
||||
HAS_OPENSSH_SUPPORT,
|
||||
HAS_OPENSSH_PRIVATE_FORMAT,
|
||||
@@ -39,320 +36,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 +371,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)))
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
acme_account_info.py
|
||||
@@ -31,9 +31,8 @@ options:
|
||||
by the ACME server."
|
||||
- "A value of C(ignore) will not fetch the list of orders."
|
||||
- "If the value is not C(ignore) and the ACME server supports orders, the C(order_uris)
|
||||
return value is always populated. The C(orders) return value currently depends on
|
||||
whether this option is set to C(url_list) or C(object_list). In community.crypto 2.0.0,
|
||||
it will only be returned if this option is set to C(object_list)."
|
||||
return value is always populated. The C(orders) return value is only returned
|
||||
if this option is set to C(object_list)."
|
||||
- "Currently, Let's Encrypt does not return orders, so the C(orders) result
|
||||
will always be empty."
|
||||
type: str
|
||||
@@ -125,12 +124,9 @@ account:
|
||||
orders:
|
||||
description:
|
||||
- "The list of orders."
|
||||
- "If I(retrieve_orders) is C(url_list), this will be a list of URLs. In community.crypto 2.0.0,
|
||||
this return value will no longer be returned for C(url_list)."
|
||||
- "If I(retrieve_orders) is C(object_list), this will be a list of objects."
|
||||
type: list
|
||||
#elements: ... depends on retrieve_orders
|
||||
returned: if account exists, I(retrieve_orders) is not C(ignore), and server supports order listing
|
||||
elements: dict
|
||||
returned: if account exists, I(retrieve_orders) is C(object_list), and server supports order listing
|
||||
contains:
|
||||
status:
|
||||
description: The order's status.
|
||||
@@ -282,9 +278,6 @@ def main():
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
if module._name in ('acme_account_facts', 'community.crypto.acme_account_facts'):
|
||||
module.deprecate("The 'acme_account_facts' module has been renamed to 'acme_account_info'",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
backend = create_backend(module, True)
|
||||
|
||||
try:
|
||||
@@ -313,13 +306,6 @@ def main():
|
||||
if account_data.get('orders') and module.params['retrieve_orders'] != 'ignore':
|
||||
orders = get_orders_list(module, client, account_data['orders'])
|
||||
result['order_uris'] = orders
|
||||
if module.params['retrieve_orders'] == 'url_list':
|
||||
module.deprecate(
|
||||
'retrieve_orders=url_list now returns the order URI list as `order_uris`.'
|
||||
' Right now it also returns this list as `orders` for backwards compatibility,'
|
||||
' but this will stop in community.crypto 2.0.0',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
result['orders'] = orders
|
||||
if module.params['retrieve_orders'] == 'object_list':
|
||||
result['orders'] = [get_order(client, order) for order in orders]
|
||||
module.exit_json(**result)
|
||||
|
||||
@@ -580,9 +580,12 @@ class ACMECertificateClient(object):
|
||||
|
||||
if self.module.params['select_chain']:
|
||||
for criterium_idx, criterium in enumerate(self.module.params['select_chain']):
|
||||
self.select_chain_matcher.append(
|
||||
self.client.backend.create_chain_matcher(
|
||||
Criterium(criterium, index=criterium_idx)))
|
||||
try:
|
||||
self.select_chain_matcher.append(
|
||||
self.client.backend.create_chain_matcher(
|
||||
Criterium(criterium, index=criterium_idx)))
|
||||
except ValueError as exc:
|
||||
self.module.warn('Error while parsing criterium: {error}. Ignoring criterium.'.format(error=exc))
|
||||
|
||||
# Make sure account exists
|
||||
modify_account = module.params['modify_account']
|
||||
|
||||
@@ -143,7 +143,7 @@ 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.acme.errors import ModuleFailException
|
||||
|
||||
@@ -202,7 +202,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,7 @@ 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.crypto.pem import (
|
||||
split_pem_list,
|
||||
|
||||
@@ -522,7 +522,7 @@ 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.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,
|
||||
|
||||
@@ -14,10 +14,7 @@ author: "John Westcott IV (@john-westcott-iv)"
|
||||
short_description: Get a certificate from a host:port
|
||||
description:
|
||||
- Makes a secure connection and returns information about the presented certificate
|
||||
- "The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL
|
||||
backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
|
||||
- The module uses the cryptography Python library.
|
||||
- Support SNI (L(Server Name Indication,https://en.wikipedia.org/wiki/Server_Name_Indication)) only with python >= 2.7.
|
||||
options:
|
||||
host:
|
||||
@@ -50,6 +47,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
|
||||
@@ -58,19 +63,18 @@ options:
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
choices: [ auto, cryptography ]
|
||||
|
||||
notes:
|
||||
- When using ca_cert on OS X it has been reported that in some conditions the validate will always succeed.
|
||||
|
||||
requirements:
|
||||
- "python >= 2.7 when using C(proxy_host)"
|
||||
- "cryptography >= 1.6 or pyOpenSSL >= 0.15"
|
||||
- "cryptography >= 1.6"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
@@ -95,7 +99,13 @@ extensions:
|
||||
asn1_data:
|
||||
returned: success
|
||||
type: str
|
||||
description: The Base64 encoded ASN.1 content of the extnesion.
|
||||
description:
|
||||
- The Base64 encoded ASN.1 content of the extension.
|
||||
- B(Note) that depending on the C(cryptography) version used, it is
|
||||
not possible to extract the ASN.1 content of the extension, but only
|
||||
to provide the re-encoded content of the extension in case it was
|
||||
parsed by C(cryptography). This should usually result in exactly the
|
||||
same value, except if the original extension value was malformed.
|
||||
name:
|
||||
returned: success
|
||||
type: str
|
||||
@@ -165,14 +175,13 @@ 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.crypto.cryptography_support import (
|
||||
cryptography_oid_to_name,
|
||||
cryptography_get_extensions_from_cert,
|
||||
)
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
||||
|
||||
CREATE_DEFAULT_CONTEXT_IMP_ERR = None
|
||||
@@ -184,17 +193,6 @@ except ImportError:
|
||||
else:
|
||||
HAS_CREATE_DEFAULT_CONTEXT = True
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
@@ -209,6 +207,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(
|
||||
@@ -219,7 +231,8 @@ def main():
|
||||
proxy_port=dict(type='int', default=8080),
|
||||
server_name=dict(type='str'),
|
||||
timeout=dict(type='int', default=10),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
|
||||
starttls=dict(type='str', choices=['mysql']),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -230,33 +243,23 @@ 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':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# First try cryptography, then pyOpenSSL
|
||||
# Try cryptography
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
module.fail_json(msg=("Can't detect the required Python library "
|
||||
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
@@ -305,6 +308,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:
|
||||
@@ -316,37 +322,7 @@ def main():
|
||||
|
||||
result['cert'] = cert
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
|
||||
result['subject'] = {}
|
||||
for component in x509.get_subject().get_components():
|
||||
result['subject'][component[0]] = component[1]
|
||||
|
||||
result['expired'] = x509.has_expired()
|
||||
|
||||
result['extensions'] = []
|
||||
extension_count = x509.get_extension_count()
|
||||
for index in range(0, extension_count):
|
||||
extension = x509.get_extension(index)
|
||||
result['extensions'].append({
|
||||
'critical': extension.get_critical(),
|
||||
'asn1_data': extension.get_data(),
|
||||
'name': extension.get_short_name(),
|
||||
})
|
||||
|
||||
result['issuer'] = {}
|
||||
for component in x509.get_issuer().get_components():
|
||||
result['issuer'][component[0]] = component[1]
|
||||
|
||||
result['not_after'] = x509.get_notAfter()
|
||||
result['not_before'] = x509.get_notBefore()
|
||||
|
||||
result['serial_number'] = x509.get_serial_number()
|
||||
result['signature_algorithm'] = x509.get_signature_algorithm()
|
||||
|
||||
result['version'] = x509.get_version()
|
||||
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography_backend())
|
||||
result['subject'] = {}
|
||||
for attribute in x509.subject:
|
||||
|
||||
@@ -356,6 +356,34 @@ LUKS_NAME_REGEX = re.compile(r'\s*crypt\s+([^\s]*)\s*')
|
||||
LUKS_DEVICE_REGEX = re.compile(r'\s*device:\s+([^\s]*)\s*')
|
||||
|
||||
|
||||
# See https://gitlab.com/cryptsetup/cryptsetup/-/wikis/LUKS-standard/on-disk-format.pdf
|
||||
LUKS_HEADER = b'LUKS\xba\xbe'
|
||||
LUKS_HEADER_L = 6
|
||||
# See https://gitlab.com/cryptsetup/LUKS2-docs/-/blob/master/luks2_doc_wip.pdf
|
||||
LUKS2_HEADER_OFFSETS = [0x4000, 0x8000, 0x10000, 0x20000, 0x40000, 0x80000, 0x100000, 0x200000, 0x400000]
|
||||
LUKS2_HEADER2 = b'SKUL\xba\xbe'
|
||||
|
||||
|
||||
def wipe_luks_headers(device):
|
||||
wipe_offsets = []
|
||||
with open(device, 'rb') as f:
|
||||
# f.seek(0)
|
||||
data = f.read(LUKS_HEADER_L)
|
||||
if data == LUKS_HEADER:
|
||||
wipe_offsets.append(0)
|
||||
for offset in LUKS2_HEADER_OFFSETS:
|
||||
f.seek(offset)
|
||||
data = f.read(LUKS_HEADER_L)
|
||||
if data == LUKS2_HEADER2:
|
||||
wipe_offsets.append(offset)
|
||||
|
||||
if wipe_offsets:
|
||||
with open(device, 'wb') as f:
|
||||
for offset in wipe_offsets:
|
||||
f.seek(offset)
|
||||
f.write(b'\x00\x00\x00\x00\x00\x00')
|
||||
|
||||
|
||||
class Handler(object):
|
||||
|
||||
def __init__(self, module):
|
||||
@@ -515,9 +543,17 @@ class CryptHandler(Handler):
|
||||
self.run_luks_close(name)
|
||||
result = self._run_command([wipefs_bin, '--all', device])
|
||||
if result[RETURN_CODE] != 0:
|
||||
raise ValueError('Error while wiping luks container %s: %s'
|
||||
raise ValueError('Error while wiping LUKS container signatures for %s: %s'
|
||||
% (device, result[STDERR]))
|
||||
|
||||
# For LUKS2, sometimes both `cryptsetup erase` and `wipefs` do **not**
|
||||
# erase all LUKS signatures (they seem to miss the second header). That's
|
||||
# why we do it ourselves here.
|
||||
try:
|
||||
wipe_luks_headers(device)
|
||||
except Exception as exc:
|
||||
raise ValueError('Error while wiping LUKS container signatures for %s: %s' % (device, exc))
|
||||
|
||||
def run_luks_add_key(self, device, keyfile, passphrase, new_keyfile,
|
||||
new_passphrase, pbkdf):
|
||||
''' Add new key from a keyfile or passphrase to given 'device';
|
||||
|
||||
@@ -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,292 @@ 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.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(),
|
||||
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
|
||||
'''
|
||||
@@ -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__':
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
x509_certificate.py
|
||||
@@ -1 +0,0 @@
|
||||
x509_certificate_info.py
|
||||
@@ -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,
|
||||
|
||||
@@ -17,13 +17,9 @@ description:
|
||||
- This module allows one to query information on OpenSSL Certificate Signing Requests (CSR).
|
||||
- In case the CSR signature cannot be validated, the module will fail. In this case, all return
|
||||
variables are still returned.
|
||||
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
|
||||
cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
|
||||
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
|
||||
C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
|
||||
and will be removed in community.crypto 2.0.0.
|
||||
- It uses the cryptography python library to interact with OpenSSL.
|
||||
requirements:
|
||||
- PyOpenSSL >= 0.15 or cryptography >= 1.3
|
||||
- cryptography >= 1.3
|
||||
author:
|
||||
- Felix Fontein (@felixfontein)
|
||||
- Yanis Guenane (@Spredzy)
|
||||
@@ -42,14 +38,11 @@ options:
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
choices: [ auto, cryptography ]
|
||||
|
||||
seealso:
|
||||
- module: community.crypto.openssl_csr
|
||||
@@ -110,7 +103,13 @@ extensions_by_oid:
|
||||
returned: success
|
||||
type: bool
|
||||
value:
|
||||
description: The Base64 encoded value (in DER format) of the extension
|
||||
description:
|
||||
- The Base64 encoded value (in DER format) of the extension.
|
||||
- B(Note) that depending on the C(cryptography) version used, it is
|
||||
not possible to extract the ASN.1 content of the extension, but only
|
||||
to provide the re-encoded content of the extension in case it was
|
||||
parsed by C(cryptography). This should usually result in exactly the
|
||||
same value, except if the original extension value was malformed.
|
||||
returned: success
|
||||
type: str
|
||||
sample: "MAMCAQU="
|
||||
@@ -267,7 +266,7 @@ subject_key_identifier:
|
||||
- The CSR's subject key identifier.
|
||||
- The identifier is returned in hexadecimal, with C(:) used to separate bytes.
|
||||
- Is C(none) if the C(SubjectKeyIdentifier) extension is not present.
|
||||
returned: success and if the pyOpenSSL backend is I(not) used
|
||||
returned: success
|
||||
type: str
|
||||
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
|
||||
authority_key_identifier:
|
||||
@@ -275,14 +274,14 @@ authority_key_identifier:
|
||||
- The CSR's authority key identifier.
|
||||
- The identifier is returned in hexadecimal, with C(:) used to separate bytes.
|
||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||
returned: success and if the pyOpenSSL backend is I(not) used
|
||||
returned: success
|
||||
type: str
|
||||
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
|
||||
authority_cert_issuer:
|
||||
description:
|
||||
- The CSR's authority cert issuer as a list of general names.
|
||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||
returned: success and if the pyOpenSSL backend is I(not) used
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
|
||||
@@ -290,14 +289,14 @@ authority_cert_serial_number:
|
||||
description:
|
||||
- The CSR's authority cert serial number.
|
||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||
returned: success and if the pyOpenSSL backend is I(not) used
|
||||
returned: success
|
||||
type: int
|
||||
sample: '12345'
|
||||
'''
|
||||
|
||||
|
||||
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,
|
||||
@@ -313,7 +312,7 @@ def main():
|
||||
argument_spec=dict(
|
||||
path=dict(type='path'),
|
||||
content=dict(type='str'),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
|
||||
),
|
||||
required_one_of=(
|
||||
['path', 'content'],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -131,7 +131,7 @@ 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.io import (
|
||||
load_file_if_exists,
|
||||
|
||||
@@ -242,7 +242,7 @@ 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.io import (
|
||||
load_file_if_exists,
|
||||
|
||||
@@ -117,7 +117,6 @@ filename:
|
||||
fingerprint:
|
||||
description:
|
||||
- The fingerprint of the public key. Fingerprint will be generated for each C(hashlib.algorithms) available.
|
||||
- The PyOpenSSL backend requires PyOpenSSL >= 16.0 for meaningful output.
|
||||
returned: changed or success
|
||||
type: dict
|
||||
sample:
|
||||
@@ -144,7 +143,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,
|
||||
|
||||
@@ -19,13 +19,9 @@ description:
|
||||
private key. In this case, all return variables are still returned. Note that key consistency
|
||||
checks are not available all key types; if none is available, C(none) is returned for
|
||||
C(key_is_consistent).
|
||||
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
|
||||
cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
|
||||
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
|
||||
C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
|
||||
and will be removed in community.crypto 2.0.0.
|
||||
- It uses the cryptography python library to interact with OpenSSL.
|
||||
requirements:
|
||||
- PyOpenSSL >= 0.15 or cryptography >= 1.2.3
|
||||
- cryptography >= 1.2.3
|
||||
author:
|
||||
- Felix Fontein (@felixfontein)
|
||||
- Yanis Guenane (@Spredzy)
|
||||
@@ -49,21 +45,29 @@ options:
|
||||
- Whether to return private key data.
|
||||
- Only set this to C(yes) when you want private information about this key to
|
||||
leave the remote machine.
|
||||
- "WARNING: you have to make sure that private key data isn't accidentally logged!"
|
||||
- "B(WARNING:) you have to make sure that private key data isn't accidentally logged!"
|
||||
type: bool
|
||||
default: no
|
||||
check_consistency:
|
||||
description:
|
||||
- Whether to check consistency of the private key.
|
||||
- In community.crypto < 2.0.0, consistency was always checked.
|
||||
- Since community.crypto 2.0.0, the consistency check has been disabled by default to
|
||||
avoid private key material to be transported around and computed with, and only do
|
||||
so when requested explicitly. This can potentially prevent
|
||||
L(side-channel attacks,https://en.wikipedia.org/wiki/Side-channel_attack).
|
||||
type: bool
|
||||
default: false
|
||||
version_added: 2.0.0
|
||||
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
choices: [ auto, cryptography ]
|
||||
|
||||
notes:
|
||||
- Supports C(check_mode).
|
||||
@@ -102,7 +106,7 @@ key_is_consistent:
|
||||
- Whether the key is consistent. Can also return C(none) next to C(yes) and
|
||||
C(no), to indicate that consistency could not be checked.
|
||||
- In case the check returns C(no), the module will fail.
|
||||
returned: always
|
||||
returned: when I(check_consistency=true)
|
||||
type: bool
|
||||
public_key:
|
||||
description: Private key's public key in PEM format.
|
||||
@@ -195,7 +199,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,
|
||||
@@ -215,7 +219,8 @@ def main():
|
||||
content=dict(type='str', no_log=True),
|
||||
passphrase=dict(type='str', no_log=True),
|
||||
return_private_key_data=dict(type='bool', default=False),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
check_consistency=dict(type='bool', default=False),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
|
||||
),
|
||||
required_one_of=(
|
||||
['path', 'content'],
|
||||
@@ -248,7 +253,8 @@ def main():
|
||||
module.params['select_crypto_backend'],
|
||||
data,
|
||||
passphrase=module.params['passphrase'],
|
||||
return_private_key_data=module.params['return_private_key_data'])
|
||||
return_private_key_data=module.params['return_private_key_data'],
|
||||
check_consistency=module.params['check_consistency'])
|
||||
|
||||
try:
|
||||
result.update(module_backend.get_info())
|
||||
|
||||
@@ -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
|
||||
@@ -97,7 +97,6 @@ curve:
|
||||
fingerprint:
|
||||
description:
|
||||
- The fingerprint of the public key. Fingerprint will be generated for each C(hashlib.algorithms) available.
|
||||
- The PyOpenSSL backend requires PyOpenSSL >= 16.0 for meaningful output.
|
||||
returned: changed or success
|
||||
type: dict
|
||||
sample:
|
||||
|
||||
@@ -15,14 +15,9 @@ short_description: Generate an OpenSSL public key from its private key.
|
||||
description:
|
||||
- This module allows one to (re)generate OpenSSL public keys from their private keys.
|
||||
- Keys are generated in PEM or OpenSSH format.
|
||||
- "The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. When I(format) is C(OpenSSH),
|
||||
the C(cryptography) backend has to be used. Please note that the PyOpenSSL backend
|
||||
was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
|
||||
- The module uses the cryptography Python library.
|
||||
requirements:
|
||||
- Either cryptography >= 1.2.3 (older versions might work as well)
|
||||
- Or pyOpenSSL >= 16.0.0
|
||||
- cryptography >= 1.2.3 (older versions might work as well)
|
||||
- Needs cryptography >= 1.4 if I(format) is C(OpenSSH)
|
||||
author:
|
||||
- Yanis Guenane (@Spredzy)
|
||||
@@ -76,12 +71,11 @@ options:
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
choices: [ auto, cryptography ]
|
||||
return_content:
|
||||
description:
|
||||
- If set to C(yes), will return the (current or generated) public key's content as I(publickey).
|
||||
@@ -157,7 +151,6 @@ filename:
|
||||
fingerprint:
|
||||
description:
|
||||
- The fingerprint of the public key. Fingerprint will be generated for each hashlib.algorithms available.
|
||||
- Requires PyOpenSSL >= 16.0 for meaningful output.
|
||||
returned: changed or success
|
||||
type: dict
|
||||
sample:
|
||||
@@ -185,7 +178,7 @@ 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.io import (
|
||||
load_file_if_exists,
|
||||
@@ -208,21 +201,9 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
|
||||
get_publickey_info,
|
||||
)
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '16.0.0'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH = '1.4'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
@@ -300,11 +281,6 @@ class PublicKey(OpenSSLObject):
|
||||
crypto_serialization.Encoding.PEM,
|
||||
crypto_serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
else:
|
||||
try:
|
||||
return crypto.dump_publickey(crypto.FILETYPE_PEM, self.privatekey)
|
||||
except AttributeError as dummy:
|
||||
raise PublicKeyError('You need to have PyOpenSSL>=16.0.0 to generate public keys')
|
||||
|
||||
def generate(self, module):
|
||||
"""Generate the public key."""
|
||||
@@ -372,11 +348,6 @@ class PublicKey(OpenSSLObject):
|
||||
crypto_serialization.Encoding.PEM,
|
||||
crypto_serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
else:
|
||||
publickey_content = crypto.dump_publickey(
|
||||
crypto.FILETYPE_PEM,
|
||||
crypto.load_publickey(crypto.FILETYPE_PEM, publickey_content)
|
||||
)
|
||||
except Exception as dummy:
|
||||
return False
|
||||
|
||||
@@ -434,7 +405,7 @@ def main():
|
||||
format=dict(type='str', default='PEM', choices=['OpenSSH', 'PEM']),
|
||||
privatekey_passphrase=dict(type='str', no_log=True),
|
||||
backup=dict(type='bool', default=False),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
|
||||
return_content=dict(type='bool', default=False),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
@@ -453,36 +424,20 @@ def main():
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(minimal_cryptography_version)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# Decision
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
if module.params['format'] == 'OpenSSH':
|
||||
module.fail_json(
|
||||
msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR
|
||||
)
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
minimal_cryptography_version,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
module.fail_json(msg=("Can't detect the required Python library "
|
||||
"cryptography (>= {0})").format(minimal_cryptography_version))
|
||||
|
||||
if module.params['format'] == 'OpenSSH' and backend != 'cryptography':
|
||||
module.fail_json(msg="Format OpenSSH requires the cryptography backend.")
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(minimal_cryptography_version)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
|
||||
@@ -14,14 +14,10 @@ module: openssl_publickey_info
|
||||
short_description: Provide information for OpenSSL public keys
|
||||
description:
|
||||
- This module allows one to query information on OpenSSL public keys.
|
||||
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
|
||||
cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
|
||||
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
|
||||
C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
|
||||
and will be removed in community.crypto 2.0.0.
|
||||
- It uses the cryptography python library to interact with OpenSSL.
|
||||
version_added: 1.7.0
|
||||
requirements:
|
||||
- PyOpenSSL >= 0.15 or cryptography >= 1.2.3
|
||||
- cryptography >= 1.2.3
|
||||
author:
|
||||
- Felix Fontein (@felixfontein)
|
||||
options:
|
||||
@@ -38,14 +34,11 @@ options:
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
choices: [ auto, cryptography ]
|
||||
|
||||
notes:
|
||||
- Supports C(check_mode).
|
||||
@@ -61,7 +54,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 +150,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,
|
||||
@@ -174,7 +167,7 @@ def main():
|
||||
argument_spec=dict(
|
||||
path=dict(type='path'),
|
||||
content=dict(type='str', no_log=True),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
|
||||
),
|
||||
required_one_of=(
|
||||
['path', 'content'],
|
||||
|
||||
@@ -15,13 +15,9 @@ version_added: 1.1.0
|
||||
short_description: Sign data with openssl
|
||||
description:
|
||||
- This module allows one to sign data using a private key.
|
||||
- The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL backend
|
||||
was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0.
|
||||
- The module uses the cryptography Python library.
|
||||
requirements:
|
||||
- Either cryptography >= 1.4 (some key types require newer versions)
|
||||
- Or pyOpenSSL >= 0.11 (Ed25519 and Ed448 keys are not supported with this backend)
|
||||
- cryptography >= 1.4 (some key types require newer versions)
|
||||
author:
|
||||
- Patrick Pichler (@aveexy)
|
||||
- Markus Teufelberger (@MarkusTeufelberger)
|
||||
@@ -50,12 +46,11 @@ options:
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
choices: [ auto, cryptography ]
|
||||
notes:
|
||||
- |
|
||||
When using the C(cryptography) backend, the following key types require at least the following C(cryptography) version:
|
||||
@@ -99,20 +94,8 @@ import traceback
|
||||
from distutils.version import LooseVersion
|
||||
import base64
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.11'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
@@ -139,7 +122,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
|
||||
|
||||
|
||||
@@ -170,34 +153,6 @@ class SignatureBase(OpenSSLObject):
|
||||
pass
|
||||
|
||||
|
||||
# Implementation with using pyOpenSSL
|
||||
class SignaturePyOpenSSL(SignatureBase):
|
||||
|
||||
def __init__(self, module, backend):
|
||||
super(SignaturePyOpenSSL, self).__init__(module, backend)
|
||||
|
||||
def run(self):
|
||||
|
||||
result = dict()
|
||||
|
||||
try:
|
||||
with open(self.path, "rb") as f:
|
||||
_in = f.read()
|
||||
|
||||
private_key = load_privatekey(
|
||||
path=self.privatekey_path,
|
||||
content=self.privatekey_content,
|
||||
passphrase=self.privatekey_passphrase,
|
||||
backend=self.backend,
|
||||
)
|
||||
|
||||
signature = OpenSSL.crypto.sign(private_key, _in, "sha256")
|
||||
result['signature'] = base64.b64encode(signature)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError(e)
|
||||
|
||||
|
||||
# Implementation with using cryptography
|
||||
class SignatureCryptography(SignatureBase):
|
||||
|
||||
@@ -262,7 +217,7 @@ def main():
|
||||
privatekey_content=dict(type='str', no_log=True),
|
||||
privatekey_passphrase=dict(type='str', no_log=True),
|
||||
path=dict(type='path', required=True),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
|
||||
),
|
||||
mutually_exclusive=(
|
||||
['privatekey_path', 'privatekey_content'],
|
||||
@@ -283,29 +238,17 @@ def main():
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# Decision
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
module.fail_json(msg=("Can't detect the required Python library "
|
||||
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||
try:
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
_sign = SignaturePyOpenSSL(module, backend)
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
|
||||
@@ -15,13 +15,9 @@ version_added: 1.1.0
|
||||
short_description: Verify signatures with openssl
|
||||
description:
|
||||
- This module allows one to verify a signature for a file by a certificate.
|
||||
- The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||
library. By default, it tries to detect which one is available. This can be
|
||||
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL backend
|
||||
was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0.
|
||||
- The module uses the cryptography Python library.
|
||||
requirements:
|
||||
- Either cryptography >= 1.4 (some key types require newer versions)
|
||||
- Or pyOpenSSL >= 0.11 (Ed25519 and Ed448 keys are not supported with this backend)
|
||||
- cryptography >= 1.4 (some key types require newer versions)
|
||||
author:
|
||||
- Patrick Pichler (@aveexy)
|
||||
- Markus Teufelberger (@MarkusTeufelberger)
|
||||
@@ -49,12 +45,11 @@ options:
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
choices: [ auto, cryptography ]
|
||||
notes:
|
||||
- |
|
||||
When using the C(cryptography) backend, the following key types require at least the following C(cryptography) version:
|
||||
@@ -99,20 +94,8 @@ import traceback
|
||||
from distutils.version import LooseVersion
|
||||
import base64
|
||||
|
||||
MINIMAL_PYOPENSSL_VERSION = '0.11'
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
|
||||
|
||||
PYOPENSSL_IMP_ERR = None
|
||||
try:
|
||||
import OpenSSL
|
||||
from OpenSSL import crypto
|
||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||
except ImportError:
|
||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||
PYOPENSSL_FOUND = False
|
||||
else:
|
||||
PYOPENSSL_FOUND = True
|
||||
|
||||
CRYPTOGRAPHY_IMP_ERR = None
|
||||
try:
|
||||
import cryptography
|
||||
@@ -139,7 +122,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
|
||||
|
||||
|
||||
@@ -170,37 +153,6 @@ class SignatureInfoBase(OpenSSLObject):
|
||||
pass
|
||||
|
||||
|
||||
# Implementation with using pyOpenSSL
|
||||
class SignatureInfoPyOpenSSL(SignatureInfoBase):
|
||||
|
||||
def __init__(self, module, backend):
|
||||
super(SignatureInfoPyOpenSSL, self).__init__(module, backend)
|
||||
|
||||
def run(self):
|
||||
|
||||
result = dict()
|
||||
|
||||
try:
|
||||
with open(self.path, "rb") as f:
|
||||
_in = f.read()
|
||||
|
||||
_signature = base64.b64decode(self.signature)
|
||||
certificate = load_certificate(
|
||||
path=self.certificate_path,
|
||||
content=self.certificate_content,
|
||||
backend=self.backend,
|
||||
)
|
||||
|
||||
try:
|
||||
OpenSSL.crypto.verify(certificate, _signature, _in, 'sha256')
|
||||
result['valid'] = True
|
||||
except Exception:
|
||||
result['valid'] = False
|
||||
return result
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError(e)
|
||||
|
||||
|
||||
# Implementation with using cryptography
|
||||
class SignatureInfoCryptography(SignatureInfoBase):
|
||||
|
||||
@@ -295,7 +247,7 @@ def main():
|
||||
certificate_content=dict(type='str'),
|
||||
path=dict(type='path', required=True),
|
||||
signature=dict(type='str', required=True),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
|
||||
),
|
||||
mutually_exclusive=(
|
||||
['certificate_path', 'certificate_content'],
|
||||
@@ -316,29 +268,17 @@ def main():
|
||||
if backend == 'auto':
|
||||
# Detection what is possible
|
||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||
|
||||
# Decision
|
||||
if can_use_cryptography:
|
||||
backend = 'cryptography'
|
||||
elif can_use_pyopenssl:
|
||||
backend = 'pyopenssl'
|
||||
|
||||
# Success?
|
||||
if backend == 'auto':
|
||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||
MINIMAL_PYOPENSSL_VERSION))
|
||||
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
|
||||
try:
|
||||
if backend == 'pyopenssl':
|
||||
if not PYOPENSSL_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||
exception=PYOPENSSL_IMP_ERR)
|
||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
_sign = SignatureInfoPyOpenSSL(module, backend)
|
||||
elif backend == 'cryptography':
|
||||
if backend == 'cryptography':
|
||||
if not CRYPTOGRAPHY_FOUND:
|
||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||
|
||||
@@ -14,7 +14,7 @@ DOCUMENTATION = r'''
|
||||
module: x509_certificate
|
||||
short_description: Generate and/or check OpenSSL certificates
|
||||
description:
|
||||
- It implements a notion of provider (ie. C(selfsigned), C(ownca), C(acme), C(assertonly), C(entrust))
|
||||
- It implements a notion of provider (one of C(selfsigned), C(ownca), C(acme), and C(entrust))
|
||||
for your certificate.
|
||||
- "Please note that the module regenerates existing certificate if it does not match the module's
|
||||
options, or if it seems to be corrupt. If you are concerned that this could overwrite
|
||||
@@ -47,8 +47,6 @@ options:
|
||||
provider:
|
||||
description:
|
||||
- Name of the provider to use to generate/retrieve the OpenSSL certificate.
|
||||
- The C(assertonly) provider will not generate files and fail if the certificate file is missing.
|
||||
- The C(assertonly) provider has been deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0.
|
||||
Please see the examples on how to emulate it with
|
||||
M(community.crypto.x509_certificate_info), M(community.crypto.openssl_csr_info),
|
||||
M(community.crypto.openssl_privatekey_info) and M(ansible.builtin.assert).
|
||||
@@ -56,7 +54,7 @@ options:
|
||||
L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API."
|
||||
- Required if I(state) is C(present).
|
||||
type: str
|
||||
choices: [ acme, assertonly, entrust, ownca, selfsigned ]
|
||||
choices: [ acme, entrust, ownca, selfsigned ]
|
||||
|
||||
return_content:
|
||||
description:
|
||||
@@ -69,9 +67,6 @@ options:
|
||||
description:
|
||||
- Create a backup file including a timestamp so you can get the original
|
||||
certificate back if you overwrote it with a new one by accident.
|
||||
- This is not used by the C(assertonly) provider.
|
||||
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
|
||||
For alternatives, see the example on replacing C(assertonly).
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
@@ -96,7 +91,6 @@ extends_documentation_fragment:
|
||||
- ansible.builtin.files
|
||||
- community.crypto.module_certificate
|
||||
- community.crypto.module_certificate.backend_acme_documentation
|
||||
- community.crypto.module_certificate.backend_assertonly_documentation
|
||||
- community.crypto.module_certificate.backend_entrust_documentation
|
||||
- community.crypto.module_certificate.backend_ownca_documentation
|
||||
- community.crypto.module_certificate.backend_selfsigned_documentation
|
||||
@@ -150,40 +144,9 @@ EXAMPLES = r'''
|
||||
entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-key.crt
|
||||
entrust_api_specification_path: /etc/ssl/entrust/api-docs/cms-api-2.1.0.yaml
|
||||
|
||||
# The following example shows one assertonly usage using all existing options for
|
||||
# assertonly, and shows how to emulate the behavior with the x509_certificate_info,
|
||||
# openssl_csr_info, openssl_privatekey_info and assert modules:
|
||||
- name: Usage of assertonly with all existing options
|
||||
community.crypto.x509_certificate:
|
||||
provider: assertonly
|
||||
path: /etc/ssl/crt/ansible.com.crt
|
||||
csr_path: /etc/ssl/csr/ansible.com.csr
|
||||
privatekey_path: /etc/ssl/csr/ansible.com.key
|
||||
signature_algorithms:
|
||||
- sha256WithRSAEncryption
|
||||
- sha512WithRSAEncryption
|
||||
subject:
|
||||
commonName: ansible.com
|
||||
subject_strict: yes
|
||||
issuer:
|
||||
commonName: ansible.com
|
||||
issuer_strict: yes
|
||||
has_expired: no
|
||||
version: 3
|
||||
key_usage:
|
||||
- Data Encipherment
|
||||
key_usage_strict: yes
|
||||
extended_key_usage:
|
||||
- DVCS
|
||||
extended_key_usage_strict: yes
|
||||
subject_alt_name:
|
||||
- dns:ansible.com
|
||||
subject_alt_name_strict: yes
|
||||
not_before: 20190331202428Z
|
||||
not_after: 20190413202428Z
|
||||
valid_at: "+1d10h"
|
||||
invalid_at: 20200331202428Z
|
||||
valid_in: 10 # in ten seconds
|
||||
# The following example shows how to emulate the behavior of the removed
|
||||
# "assertonly" provider with the x509_certificate_info, openssl_csr_info,
|
||||
# openssl_privatekey_info and assert modules:
|
||||
|
||||
- name: Get certificate information
|
||||
community.crypto.x509_certificate_info:
|
||||
@@ -208,9 +171,9 @@ EXAMPLES = r'''
|
||||
|
||||
- assert:
|
||||
that:
|
||||
# When private key is specified for assertonly, this will be checked:
|
||||
# When private key was specified for assertonly, this was checked:
|
||||
- result.public_key == result_privatekey.public_key
|
||||
# When CSR is specified for assertonly, this will be checked:
|
||||
# When CSR was specified for assertonly, this was checked:
|
||||
- result.public_key == result_csr.public_key
|
||||
- result.subject_ordered == result_csr.subject_ordered
|
||||
- result.extensions_by_oid == result_csr.extensions_by_oid
|
||||
@@ -242,103 +205,6 @@ EXAMPLES = r'''
|
||||
- "result.valid_at.one_day_ten_hours" # for valid_at
|
||||
- "not result.valid_at.fixed_timestamp" # for invalid_at
|
||||
- "result.valid_at.ten_seconds" # for valid_in
|
||||
|
||||
# Examples for some checks one could use the assertonly provider for:
|
||||
# (Please note that assertonly has been deprecated!)
|
||||
|
||||
# How to use the assertonly provider to implement and trigger your own custom certificate generation workflow:
|
||||
- name: Check if a certificate is currently still valid, ignoring failures
|
||||
community.crypto.x509_certificate:
|
||||
path: /etc/ssl/crt/example.com.crt
|
||||
provider: assertonly
|
||||
has_expired: no
|
||||
ignore_errors: yes
|
||||
register: validity_check
|
||||
|
||||
- name: Run custom task(s) to get a new, valid certificate in case the initial check failed
|
||||
command: superspecialSSL recreate /etc/ssl/crt/example.com.crt
|
||||
when: validity_check.failed
|
||||
|
||||
- name: Check the new certificate again for validity with the same parameters, this time failing the play if it is still invalid
|
||||
community.crypto.x509_certificate:
|
||||
path: /etc/ssl/crt/example.com.crt
|
||||
provider: assertonly
|
||||
has_expired: no
|
||||
when: validity_check.failed
|
||||
|
||||
# Some other checks that assertonly could be used for:
|
||||
- name: Verify that an existing certificate was issued by the Let's Encrypt CA and is currently still valid
|
||||
community.crypto.x509_certificate:
|
||||
path: /etc/ssl/crt/example.com.crt
|
||||
provider: assertonly
|
||||
issuer:
|
||||
O: Let's Encrypt
|
||||
has_expired: no
|
||||
|
||||
- name: Ensure that a certificate uses a modern signature algorithm (no SHA1, MD5 or DSA)
|
||||
community.crypto.x509_certificate:
|
||||
path: /etc/ssl/crt/example.com.crt
|
||||
provider: assertonly
|
||||
signature_algorithms:
|
||||
- sha224WithRSAEncryption
|
||||
- sha256WithRSAEncryption
|
||||
- sha384WithRSAEncryption
|
||||
- sha512WithRSAEncryption
|
||||
- sha224WithECDSAEncryption
|
||||
- sha256WithECDSAEncryption
|
||||
- sha384WithECDSAEncryption
|
||||
- sha512WithECDSAEncryption
|
||||
|
||||
- name: Ensure that the existing certificate belongs to the specified private key
|
||||
community.crypto.x509_certificate:
|
||||
path: /etc/ssl/crt/example.com.crt
|
||||
privatekey_path: /etc/ssl/private/example.com.pem
|
||||
provider: assertonly
|
||||
|
||||
- name: Ensure that the existing certificate is still valid at the winter solstice 2017
|
||||
community.crypto.x509_certificate:
|
||||
path: /etc/ssl/crt/example.com.crt
|
||||
provider: assertonly
|
||||
valid_at: 20171221162800Z
|
||||
|
||||
- name: Ensure that the existing certificate is still valid 2 weeks (1209600 seconds) from now
|
||||
community.crypto.x509_certificate:
|
||||
path: /etc/ssl/crt/example.com.crt
|
||||
provider: assertonly
|
||||
valid_in: 1209600
|
||||
|
||||
- name: Ensure that the existing certificate is only used for digital signatures and encrypting other keys
|
||||
community.crypto.x509_certificate:
|
||||
path: /etc/ssl/crt/example.com.crt
|
||||
provider: assertonly
|
||||
key_usage:
|
||||
- digitalSignature
|
||||
- keyEncipherment
|
||||
key_usage_strict: true
|
||||
|
||||
- name: Ensure that the existing certificate can be used for client authentication
|
||||
community.crypto.x509_certificate:
|
||||
path: /etc/ssl/crt/example.com.crt
|
||||
provider: assertonly
|
||||
extended_key_usage:
|
||||
- clientAuth
|
||||
|
||||
- name: Ensure that the existing certificate can only be used for client authentication and time stamping
|
||||
community.crypto.x509_certificate:
|
||||
path: /etc/ssl/crt/example.com.crt
|
||||
provider: assertonly
|
||||
extended_key_usage:
|
||||
- clientAuth
|
||||
- 1.3.6.1.5.5.7.3.8
|
||||
extended_key_usage_strict: true
|
||||
|
||||
- name: Ensure that the existing certificate has a certain domain in its subjectAltName
|
||||
community.crypto.x509_certificate:
|
||||
path: /etc/ssl/crt/example.com.crt
|
||||
provider: assertonly
|
||||
subject_alt_name:
|
||||
- www.example.com
|
||||
- test.example.com
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
@@ -362,7 +228,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,
|
||||
@@ -374,11 +240,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
|
||||
add_acme_provider_to_argument_spec,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_assertonly import (
|
||||
AssertOnlyCertificateProvider,
|
||||
add_assertonly_provider_to_argument_spec,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_entrust import (
|
||||
EntrustCertificateProvider,
|
||||
add_entrust_provider_to_argument_spec,
|
||||
@@ -495,7 +356,6 @@ class GenericCertificate(OpenSSLObject):
|
||||
def main():
|
||||
argument_spec = get_certificate_argument_spec()
|
||||
add_acme_provider_to_argument_spec(argument_spec)
|
||||
add_assertonly_provider_to_argument_spec(argument_spec)
|
||||
add_entrust_provider_to_argument_spec(argument_spec)
|
||||
add_ownca_provider_to_argument_spec(argument_spec)
|
||||
add_selfsigned_provider_to_argument_spec(argument_spec)
|
||||
@@ -511,10 +371,6 @@ def main():
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
if module._name == 'community.crypto.openssl_certificate':
|
||||
module.deprecate("The 'community.crypto.openssl_certificate' module has been renamed to 'community.crypto.x509_certificate'",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
try:
|
||||
if module.params['state'] == 'absent':
|
||||
certificate = CertificateAbsent(module)
|
||||
@@ -537,7 +393,6 @@ def main():
|
||||
provider = module.params['provider']
|
||||
provider_map = {
|
||||
'acme': AcmeCertificateProvider,
|
||||
'assertonly': AssertOnlyCertificateProvider,
|
||||
'entrust': EntrustCertificateProvider,
|
||||
'ownca': OwnCACertificateProvider,
|
||||
'selfsigned': SelfSignedCertificateProvider,
|
||||
|
||||
@@ -15,11 +15,7 @@ module: x509_certificate_info
|
||||
short_description: Provide information of OpenSSL X.509 certificates
|
||||
description:
|
||||
- This module allows one to query information on OpenSSL certificates.
|
||||
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
|
||||
cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
|
||||
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
|
||||
C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
|
||||
and will be removed in community.crypto 2.0.0.
|
||||
- It uses the cryptography python library to interact with OpenSSL.
|
||||
- Note that this module was called C(openssl_certificate_info) when included directly in Ansible
|
||||
up to version 2.9. When moved to the collection C(community.crypto), it was renamed to
|
||||
M(community.crypto.x509_certificate_info). From Ansible 2.10 on, it can still be used by the
|
||||
@@ -29,7 +25,7 @@ description:
|
||||
keyword, the new name M(community.crypto.x509_certificate_info) should be used to avoid
|
||||
a deprecation warning.
|
||||
requirements:
|
||||
- PyOpenSSL >= 0.15 or cryptography >= 1.6
|
||||
- cryptography >= 1.6
|
||||
author:
|
||||
- Felix Fontein (@felixfontein)
|
||||
- Yanis Guenane (@Spredzy)
|
||||
@@ -60,14 +56,11 @@ options:
|
||||
select_crypto_backend:
|
||||
description:
|
||||
- Determines which crypto backend to use.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||
- The default choice is C(auto), which tries to use C(cryptography) if available.
|
||||
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||
From that point on, only the C(cryptography) backend will be available.
|
||||
type: str
|
||||
default: auto
|
||||
choices: [ auto, cryptography, pyopenssl ]
|
||||
choices: [ auto, cryptography ]
|
||||
|
||||
notes:
|
||||
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
|
||||
@@ -154,7 +147,13 @@ extensions_by_oid:
|
||||
returned: success
|
||||
type: bool
|
||||
value:
|
||||
description: The Base64 encoded value (in DER format) of the extension.
|
||||
description:
|
||||
- The Base64 encoded value (in DER format) of the extension.
|
||||
- B(Note) that depending on the C(cryptography) version used, it is
|
||||
not possible to extract the ASN.1 content of the extension, but only
|
||||
to provide the re-encoded content of the extension in case it was
|
||||
parsed by C(cryptography). This should usually result in exactly the
|
||||
same value, except if the original extension value was malformed.
|
||||
returned: success
|
||||
type: str
|
||||
sample: "MAMCAQU="
|
||||
@@ -341,7 +340,7 @@ subject_key_identifier:
|
||||
- The certificate's subject key identifier.
|
||||
- The identifier is returned in hexadecimal, with C(:) used to separate bytes.
|
||||
- Is C(none) if the C(SubjectKeyIdentifier) extension is not present.
|
||||
returned: success and if the pyOpenSSL backend is I(not) used
|
||||
returned: success
|
||||
type: str
|
||||
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
|
||||
authority_key_identifier:
|
||||
@@ -349,14 +348,14 @@ authority_key_identifier:
|
||||
- The certificate's authority key identifier.
|
||||
- The identifier is returned in hexadecimal, with C(:) used to separate bytes.
|
||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||
returned: success and if the pyOpenSSL backend is I(not) used
|
||||
returned: success
|
||||
type: str
|
||||
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
|
||||
authority_cert_issuer:
|
||||
description:
|
||||
- The certificate's authority cert issuer as a list of general names.
|
||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||
returned: success and if the pyOpenSSL backend is I(not) used
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
|
||||
@@ -364,7 +363,7 @@ authority_cert_serial_number:
|
||||
description:
|
||||
- The certificate's authority cert serial number.
|
||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||
returned: success and if the pyOpenSSL backend is I(not) used
|
||||
returned: success
|
||||
type: int
|
||||
sample: '12345'
|
||||
ocsp_uri:
|
||||
@@ -377,7 +376,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,
|
||||
@@ -398,7 +397,7 @@ def main():
|
||||
path=dict(type='path'),
|
||||
content=dict(type='str'),
|
||||
valid_at=dict(type='dict'),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
|
||||
),
|
||||
required_one_of=(
|
||||
['path', 'content'],
|
||||
@@ -408,9 +407,6 @@ def main():
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
if module._name == 'community.crypto.openssl_certificate_info':
|
||||
module.deprecate("The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_certificate_info'",
|
||||
version='2.0.0', collection_name='community.crypto')
|
||||
|
||||
if module.params['content'] is not None:
|
||||
data = module.params['content'].encode('utf-8')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -92,8 +92,21 @@ options:
|
||||
description:
|
||||
- Key/value pairs that will be present in the issuer name field of the CRL.
|
||||
- If you need to specify more than one value with the same key, use a list as value.
|
||||
- Required if I(state) is C(present).
|
||||
- If the order of the components is important, use I(issuer_ordered).
|
||||
- One of I(issuer) and I(issuer_ordered) is required if I(state) is C(present).
|
||||
- Mutually exclusive with I(issuer_ordered).
|
||||
type: dict
|
||||
issuer_ordered:
|
||||
description:
|
||||
- A list of dictionaries, where every dictionary must contain one key/value pair.
|
||||
This key/value pair will be present in the issuer name field of the CRL.
|
||||
- If you want to specify more than one value with the same key in a row, you can
|
||||
use a list as value.
|
||||
- One of I(issuer) and I(issuer_ordered) is required if I(state) is C(present).
|
||||
- Mutually exclusive with I(issuer).
|
||||
type: list
|
||||
elements: dict
|
||||
version_added: 2.0.0
|
||||
|
||||
last_update:
|
||||
description:
|
||||
@@ -370,7 +383,7 @@ 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.io import (
|
||||
write_file,
|
||||
@@ -386,6 +399,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
||||
load_privatekey,
|
||||
load_certificate,
|
||||
parse_name_field,
|
||||
parse_ordered_name_field,
|
||||
get_relative_time_option,
|
||||
select_message_digest,
|
||||
)
|
||||
@@ -462,8 +476,15 @@ class CRL(OpenSSLObject):
|
||||
self.privatekey_content = self.privatekey_content.encode('utf-8')
|
||||
self.privatekey_passphrase = module.params['privatekey_passphrase']
|
||||
|
||||
self.issuer = parse_name_field(module.params['issuer'])
|
||||
self.issuer = [(entry[0], entry[1]) for entry in self.issuer if entry[1]]
|
||||
try:
|
||||
if module.params['issuer_ordered']:
|
||||
self.issuer_ordered = True
|
||||
self.issuer = parse_ordered_name_field(module.params['issuer_ordered'], 'issuer_ordered')
|
||||
else:
|
||||
self.issuer_ordered = False
|
||||
self.issuer = parse_name_field(module.params['issuer'], 'issuer')
|
||||
except (TypeError, ValueError) as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
self.last_update = get_relative_time_option(module.params['last_update'], 'last_update')
|
||||
self.next_update = get_relative_time_option(module.params['next_update'], 'next_update')
|
||||
@@ -597,7 +618,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)
|
||||
@@ -616,7 +637,11 @@ class CRL(OpenSSLObject):
|
||||
return False
|
||||
|
||||
want_issuer = [(cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer]
|
||||
if want_issuer != [(sub.oid, sub.value) for sub in self.crl.issuer]:
|
||||
is_issuer = [(sub.oid, sub.value) for sub in self.crl.issuer]
|
||||
if not self.issuer_ordered:
|
||||
want_issuer = set(want_issuer)
|
||||
is_issuer = set(is_issuer)
|
||||
if want_issuer != is_issuer:
|
||||
return False
|
||||
|
||||
old_entries = [self._compress_entry(cryptography_decode_revoked_certificate(cert)) for cert in self.crl]
|
||||
@@ -689,9 +714,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:
|
||||
@@ -782,6 +807,7 @@ def main():
|
||||
privatekey_content=dict(type='str', no_log=True),
|
||||
privatekey_passphrase=dict(type='str', no_log=True),
|
||||
issuer=dict(type='dict'),
|
||||
issuer_ordered=dict(type='list', elements='dict'),
|
||||
last_update=dict(type='str', default='+0s'),
|
||||
next_update=dict(type='str'),
|
||||
digest=dict(type='str', default='sha256'),
|
||||
@@ -815,10 +841,12 @@ def main():
|
||||
),
|
||||
required_if=[
|
||||
('state', 'present', ['privatekey_path', 'privatekey_content'], True),
|
||||
('state', 'present', ['issuer', 'next_update', 'revoked_certificates'], False),
|
||||
('state', 'present', ['issuer', 'issuer_ordered'], True),
|
||||
('state', 'present', ['next_update', 'revoked_certificates'], False),
|
||||
],
|
||||
mutually_exclusive=(
|
||||
['privatekey_path', 'privatekey_content'],
|
||||
['issuer', 'issuer_ordered'],
|
||||
),
|
||||
supports_check_mode=True,
|
||||
add_file_common_args=True,
|
||||
@@ -834,7 +862,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:
|
||||
@@ -119,21 +167,17 @@
|
||||
assert:
|
||||
that:
|
||||
- "'account' in account_orders_urls"
|
||||
- "'orders' in account_orders_urls"
|
||||
- "account_orders_urls.orders[0] is string"
|
||||
- "'orders' not in account_orders_urls"
|
||||
- "'order_uris' in account_orders_urls"
|
||||
- "account_orders_urls.order_uris[0] is string"
|
||||
- "account_orders_urls.order_uris == account_orders_urls.orders"
|
||||
|
||||
- name: Validate that orders were retrieved as list of URLs (2/2)
|
||||
assert:
|
||||
that:
|
||||
- "'account' in account_orders_urls2"
|
||||
- "'orders' in account_orders_urls2"
|
||||
- "account_orders_urls2.orders[0] is string"
|
||||
- "'orders' not in account_orders_urls2"
|
||||
- "'order_uris' in account_orders_urls2"
|
||||
- "account_orders_urls2.order_uris[0] is string"
|
||||
- "account_orders_urls2.order_uris == account_orders_urls2.orders"
|
||||
|
||||
- name: Validate that orders were retrieved as list of objects (1/2)
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user