mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
928cb3aa9b | ||
|
|
3e6815d73f | ||
|
|
cb08f56066 | ||
|
|
e3f486a063 | ||
|
|
0a1e25e16a | ||
|
|
ff4966ad3f | ||
|
|
0cb10be2d5 | ||
|
|
901863989b | ||
|
|
99377764c1 | ||
|
|
426d70fbcf | ||
|
|
f315722b31 | ||
|
|
73afe8e742 | ||
|
|
db67b8a857 | ||
|
|
e05475d58a | ||
|
|
dceee8f50e | ||
|
|
b893252ad1 | ||
|
|
0755a2b657 | ||
|
|
90bf8b0b2e | ||
|
|
5ff28c751d | ||
|
|
7b08edb5a4 | ||
|
|
fbadcbeb29 | ||
|
|
e991375f55 | ||
|
|
33c99014ae | ||
|
|
fbd6ff6ead | ||
|
|
c4ab2eb3b5 | ||
|
|
44b6df0ce5 | ||
|
|
14a42505a9 | ||
|
|
44cbd33cb7 | ||
|
|
4411a71d06 | ||
|
|
bfe37bc668 | ||
|
|
d784e0a52b | ||
|
|
d73a2942a2 | ||
|
|
8af4847373 | ||
|
|
44f7367e21 | ||
|
|
0733b0d521 | ||
|
|
771a9eebcf | ||
|
|
0fdede5d7a | ||
|
|
56b2130c6e | ||
|
|
6c018b94da | ||
|
|
63f4598737 | ||
|
|
598cdf0a21 | ||
|
|
eea7bfc6bf | ||
|
|
8521c96e8a | ||
|
|
d90cc5142b | ||
|
|
37aab65396 | ||
|
|
baff003ea8 | ||
|
|
03427e35a7 | ||
|
|
170fa40014 | ||
|
|
330b30d5d2 | ||
|
|
67b8274faf | ||
|
|
02ee3fb974 | ||
|
|
93ced1956c | ||
|
|
a9e358ea57 | ||
|
|
ffcdbc5d0c | ||
|
|
6740cae10f | ||
|
|
915379459d | ||
|
|
a4a12bae27 | ||
|
|
94fc356338 | ||
|
|
08ada24a53 | ||
|
|
b59846b9fa | ||
|
|
c9ec463893 | ||
|
|
38ce150f80 | ||
|
|
408b538a45 | ||
|
|
1e82465559 | ||
|
|
85ac60e2c3 | ||
|
|
aaba87ac57 | ||
|
|
d6403ace6e | ||
|
|
6c989de994 | ||
|
|
4908f1a8ec | ||
|
|
f3c6c1172e | ||
|
|
9658a34605 | ||
|
|
5d153e05ef | ||
|
|
2ba77e015c | ||
|
|
9d958033a5 | ||
|
|
4adad5d98a | ||
|
|
b9737101cd | ||
|
|
9f27e28a45 | ||
|
|
9a7b2f1d0d | ||
|
|
0df33de73e | ||
|
|
cda2edf92c | ||
|
|
d38f59c18c | ||
|
|
4a7150204c | ||
|
|
bfb8e5df82 | ||
|
|
376d7cde12 | ||
|
|
a466df9c52 | ||
|
|
117438cff0 | ||
|
|
c6483751b5 | ||
|
|
2bf0bb5fb3 | ||
|
|
e9bc7c7163 | ||
|
|
0a0d0f2bdf | ||
|
|
3293b77f18 | ||
|
|
69aeb2d86f | ||
|
|
7298c1f49a | ||
|
|
ba03580659 | ||
|
|
a93f07c651 | ||
|
|
c0edfb46bb | ||
|
|
6c5a0c6df1 | ||
|
|
80d64e7b64 | ||
|
|
3e7362200a | ||
|
|
c400744040 | ||
|
|
91552d5fd2 | ||
|
|
6100d9b4df | ||
|
|
37c1540ff4 | ||
|
|
3239701ba4 | ||
|
|
81408bb853 | ||
|
|
6414301936 | ||
|
|
db513d1b27 | ||
|
|
0ecdf2ccbd | ||
|
|
c05e20cf1e | ||
|
|
f4334d7307 | ||
|
|
201920d161 | ||
|
|
e809ee19ee | ||
|
|
4684f36c38 | ||
|
|
bb3ddf1961 | ||
|
|
0e1f0fd730 | ||
|
|
7b1d4770e9 | ||
|
|
befa690d9e | ||
|
|
bcf2a17257 | ||
|
|
8c6b28cd81 |
@@ -19,6 +19,11 @@ schedules:
|
|||||||
branches:
|
branches:
|
||||||
include:
|
include:
|
||||||
- main
|
- main
|
||||||
|
- cron: 0 12 * * 0
|
||||||
|
displayName: Weekly (old stable branches)
|
||||||
|
always: true
|
||||||
|
branches:
|
||||||
|
include:
|
||||||
- stable-*
|
- stable-*
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
@@ -36,7 +41,7 @@ variables:
|
|||||||
resources:
|
resources:
|
||||||
containers:
|
containers:
|
||||||
- container: default
|
- container: default
|
||||||
image: quay.io/ansible/azure-pipelines-test-container:1.8.0
|
image: quay.io/ansible/azure-pipelines-test-container:1.9.0
|
||||||
|
|
||||||
pool: Standard
|
pool: Standard
|
||||||
|
|
||||||
@@ -55,6 +60,28 @@ stages:
|
|||||||
test: 'devel/sanity/extra'
|
test: 'devel/sanity/extra'
|
||||||
- name: Units
|
- name: Units
|
||||||
test: 'devel/units/1'
|
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: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
targets:
|
||||||
|
- name: Sanity
|
||||||
|
test: '2.11/sanity/1'
|
||||||
|
- name: Units
|
||||||
|
test: '2.11/units/1'
|
||||||
- stage: Ansible_2_10
|
- stage: Ansible_2_10
|
||||||
displayName: Sanity & Units 2.10
|
displayName: Sanity & Units 2.10
|
||||||
dependsOn: []
|
dependsOn: []
|
||||||
@@ -86,16 +113,12 @@ stages:
|
|||||||
parameters:
|
parameters:
|
||||||
testFormat: devel/linux/{0}/1
|
testFormat: devel/linux/{0}/1
|
||||||
targets:
|
targets:
|
||||||
- name: CentOS 6
|
|
||||||
test: centos6
|
|
||||||
- name: CentOS 7
|
- name: CentOS 7
|
||||||
test: centos7
|
test: centos7
|
||||||
- name: CentOS 8
|
- name: Fedora 34
|
||||||
test: centos8
|
test: fedora34
|
||||||
- name: Fedora 32
|
- name: Fedora 35
|
||||||
test: fedora32
|
test: fedora35
|
||||||
- name: Fedora 33
|
|
||||||
test: fedora33
|
|
||||||
- name: openSUSE 15 py2
|
- name: openSUSE 15 py2
|
||||||
test: opensuse15py2
|
test: opensuse15py2
|
||||||
- name: openSUSE 15 py3
|
- name: openSUSE 15 py3
|
||||||
@@ -104,6 +127,42 @@ stages:
|
|||||||
test: ubuntu1804
|
test: ubuntu1804
|
||||||
- name: Ubuntu 20.04
|
- name: Ubuntu 20.04
|
||||||
test: ubuntu2004
|
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: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
testFormat: 2.11/linux/{0}/1
|
||||||
|
targets:
|
||||||
|
- name: CentOS 7
|
||||||
|
test: centos7
|
||||||
|
- name: CentOS 8
|
||||||
|
test: centos8
|
||||||
|
- name: Fedora 32
|
||||||
|
test: fedora32
|
||||||
|
- name: openSUSE 15 py2
|
||||||
|
test: opensuse15py2
|
||||||
|
- name: Ubuntu 18.04
|
||||||
|
test: ubuntu1804
|
||||||
- stage: Docker_2_10
|
- stage: Docker_2_10
|
||||||
displayName: Docker 2.10
|
displayName: Docker 2.10
|
||||||
dependsOn: []
|
dependsOn: []
|
||||||
@@ -114,24 +173,10 @@ stages:
|
|||||||
targets:
|
targets:
|
||||||
- name: CentOS 6
|
- name: CentOS 6
|
||||||
test: centos6
|
test: centos6
|
||||||
- name: CentOS 7
|
|
||||||
test: centos7
|
|
||||||
- name: CentOS 8
|
|
||||||
test: centos8
|
|
||||||
- name: Fedora 30
|
|
||||||
test: fedora30
|
|
||||||
- name: Fedora 31
|
- name: Fedora 31
|
||||||
test: fedora31
|
test: fedora31
|
||||||
- name: Fedora 32
|
|
||||||
test: fedora32
|
|
||||||
- name: openSUSE 15 py2
|
|
||||||
test: opensuse15py2
|
|
||||||
- name: openSUSE 15 py3
|
|
||||||
test: opensuse15
|
|
||||||
- name: Ubuntu 16.04
|
- name: Ubuntu 16.04
|
||||||
test: ubuntu1604
|
test: ubuntu1604
|
||||||
- name: Ubuntu 18.04
|
|
||||||
test: ubuntu1804
|
|
||||||
- stage: Docker_2_9
|
- stage: Docker_2_9
|
||||||
displayName: Docker 2.9
|
displayName: Docker 2.9
|
||||||
dependsOn: []
|
dependsOn: []
|
||||||
@@ -144,16 +189,8 @@ stages:
|
|||||||
test: centos6
|
test: centos6
|
||||||
- name: CentOS 7
|
- name: CentOS 7
|
||||||
test: centos7
|
test: centos7
|
||||||
- name: CentOS 8
|
|
||||||
test: centos8
|
|
||||||
- name: Fedora 30
|
|
||||||
test: fedora30
|
|
||||||
- name: Fedora 31
|
- name: Fedora 31
|
||||||
test: fedora31
|
test: fedora31
|
||||||
- name: openSUSE 15 py2
|
|
||||||
test: opensuse15py2
|
|
||||||
- name: openSUSE 15 py3
|
|
||||||
test: opensuse15
|
|
||||||
- name: Ubuntu 16.04
|
- name: Ubuntu 16.04
|
||||||
test: ubuntu1604
|
test: ubuntu1604
|
||||||
- name: Ubuntu 18.04
|
- name: Ubuntu 18.04
|
||||||
@@ -170,12 +207,40 @@ stages:
|
|||||||
targets:
|
targets:
|
||||||
- name: macOS 11.1
|
- name: macOS 11.1
|
||||||
test: macos/11.1
|
test: macos/11.1
|
||||||
|
- name: RHEL 7.9
|
||||||
|
test: rhel/7.9
|
||||||
|
- 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: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
testFormat: 2.11/{0}/1
|
||||||
|
targets:
|
||||||
- name: RHEL 7.9
|
- name: RHEL 7.9
|
||||||
test: rhel/7.9
|
test: rhel/7.9
|
||||||
- name: RHEL 8.3
|
- name: RHEL 8.3
|
||||||
test: rhel/8.3
|
test: rhel/8.3
|
||||||
- name: FreeBSD 11.4
|
|
||||||
test: freebsd/11.4
|
|
||||||
- name: FreeBSD 12.2
|
- name: FreeBSD 12.2
|
||||||
test: freebsd/12.2
|
test: freebsd/12.2
|
||||||
- stage: Remote_2_10
|
- stage: Remote_2_10
|
||||||
@@ -186,8 +251,6 @@ stages:
|
|||||||
parameters:
|
parameters:
|
||||||
testFormat: 2.10/{0}/1
|
testFormat: 2.10/{0}/1
|
||||||
targets:
|
targets:
|
||||||
- name: RHEL 7.8
|
|
||||||
test: rhel/7.8
|
|
||||||
- name: OS X 10.11
|
- name: OS X 10.11
|
||||||
test: osx/10.11
|
test: osx/10.11
|
||||||
- name: macOS 10.15
|
- name: macOS 10.15
|
||||||
@@ -214,13 +277,34 @@ stages:
|
|||||||
nameFormat: Python {0}
|
nameFormat: Python {0}
|
||||||
testFormat: devel/cloud/{0}/1
|
testFormat: devel/cloud/{0}/1
|
||||||
targets:
|
targets:
|
||||||
- test: 2.6
|
|
||||||
- test: 2.7
|
- test: 2.7
|
||||||
- test: 3.5
|
- test: 3.5
|
||||||
- test: 3.6
|
- test: 3.6
|
||||||
- test: 3.7
|
- test: 3.7
|
||||||
- test: 3.8
|
- test: 3.8
|
||||||
- test: 3.9
|
- 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: []
|
||||||
|
jobs:
|
||||||
|
- template: templates/matrix.yml
|
||||||
|
parameters:
|
||||||
|
nameFormat: Python {0}
|
||||||
|
testFormat: 2.11/cloud/{0}/1
|
||||||
|
targets:
|
||||||
|
- test: 3.8
|
||||||
- stage: Cloud_2_10
|
- stage: Cloud_2_10
|
||||||
displayName: Cloud 2.10
|
displayName: Cloud 2.10
|
||||||
dependsOn: []
|
dependsOn: []
|
||||||
@@ -248,16 +332,24 @@ stages:
|
|||||||
condition: succeededOrFailed()
|
condition: succeededOrFailed()
|
||||||
dependsOn:
|
dependsOn:
|
||||||
- Ansible_devel
|
- Ansible_devel
|
||||||
|
- Ansible_2_12
|
||||||
|
- Ansible_2_11
|
||||||
- Ansible_2_10
|
- Ansible_2_10
|
||||||
- Ansible_2_9
|
- Ansible_2_9
|
||||||
- Remote_devel
|
- Remote_devel
|
||||||
- Docker_devel
|
- Remote_2_12
|
||||||
- Cloud_devel
|
- Remote_2_11
|
||||||
- Remote_2_10
|
- Remote_2_10
|
||||||
- Docker_2_10
|
|
||||||
- Cloud_2_10
|
|
||||||
- Remote_2_9
|
- Remote_2_9
|
||||||
|
- Docker_devel
|
||||||
|
- Docker_2_12
|
||||||
|
- Docker_2_11
|
||||||
|
- Docker_2_10
|
||||||
- Docker_2_9
|
- Docker_2_9
|
||||||
|
- Cloud_devel
|
||||||
|
- Cloud_2_12
|
||||||
|
- Cloud_2_11
|
||||||
|
- Cloud_2_10
|
||||||
- Cloud_2_9
|
- Cloud_2_9
|
||||||
jobs:
|
jobs:
|
||||||
- template: templates/coverage.yml
|
- template: templates/coverage.yml
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ mkdir "${agent_temp_directory}/coverage/"
|
|||||||
|
|
||||||
options=(--venv --venv-system-site-packages --color -v)
|
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
|
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.
|
# 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://codecov.io/bash > codecov.sh
|
|
||||||
|
|
||||||
for file in "${output_path}"/reports/coverage*.xml; do
|
|
||||||
name="${file}"
|
|
||||||
name="${name##*/}" # remove path
|
|
||||||
name="${name##coverage=}" # remove 'coverage=' prefix if present
|
|
||||||
name="${name%.xml}" # remove '.xml' suffix
|
|
||||||
|
|
||||||
bash codecov.sh \
|
|
||||||
-f "${file}" \
|
|
||||||
-n "${name}" \
|
|
||||||
-X coveragepy \
|
|
||||||
-X gcov \
|
|
||||||
-X fix \
|
|
||||||
-X search \
|
|
||||||
-X xcode \
|
|
||||||
|| echo "Failed to upload code coverage report to codecov.io: ${file}"
|
|
||||||
done
|
|
||||||
@@ -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
|
pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check
|
||||||
fi
|
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"
|
summaryFileLocation: "$(outputPath)/reports/$(pipelinesCoverage).xml"
|
||||||
displayName: Publish to Azure Pipelines
|
displayName: Publish to Azure Pipelines
|
||||||
condition: gt(variables.coverageFileCount, 0)
|
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
|
displayName: Publish to codecov.io
|
||||||
condition: gt(variables.coverageFileCount, 0)
|
condition: gt(variables.coverageFileCount, 0)
|
||||||
continueOnError: true
|
continueOnError: true
|
||||||
|
|||||||
221
CHANGELOG.rst
221
CHANGELOG.rst
@@ -5,6 +5,227 @@ Community Crypto Release Notes
|
|||||||
.. contents:: Topics
|
.. contents:: Topics
|
||||||
|
|
||||||
|
|
||||||
|
v1.9.7
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Bugfix release with extra forward compatibility for newer versions of cryptography.
|
||||||
|
|
||||||
|
Minor Changes
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- acme_* modules - fix usage of ``fetch_url`` with changes in latest ansible-core ``devel`` branch (https://github.com/ansible-collections/community.crypto/pull/339).
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- acme_certificate - avoid passing multiple certificates to ``cryptography``'s X.509 certificate loader when ``fullchain_dest`` is used (https://github.com/ansible-collections/community.crypto/pull/324).
|
||||||
|
- get_certificate, openssl_csr_info, x509_certificate_info - add fallback code for extension parsing that works with cryptography 36.0.0 and newer. This code re-serializes de-serialized extensions and thus can return slightly different values if the extension in the original CSR resp. certificate was not canonicalized correctly. This code is currently used as a fallback if the existing code stops working, but we will switch it to be the main code in a future release (https://github.com/ansible-collections/community.crypto/pull/331).
|
||||||
|
- luks_device - now also runs a built-in LUKS signature cleaner on ``state=absent`` to make sure that also the secondary LUKS2 header is wiped when older versions of wipefs are used (https://github.com/ansible-collections/community.crypto/issues/326, https://github.com/ansible-collections/community.crypto/pull/327).
|
||||||
|
- openssl_pkcs12 - use new PKCS#12 deserialization infrastructure from cryptography 36.0.0 if available (https://github.com/ansible-collections/community.crypto/pull/302).
|
||||||
|
|
||||||
|
v1.9.6
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Regular bugfix release.
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- cryptography backend - improve Unicode handling for Python 2 (https://github.com/ansible-collections/community.crypto/pull/313).
|
||||||
|
|
||||||
|
v1.9.5
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Bugfix release to fully support cryptography 35.0.0.
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- get_certificate - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||||
|
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||||
|
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release in PyOpenSSL backend (https://github.com/ansible-collections/community.crypto/pull/300).
|
||||||
|
- openssl_pkcs12 - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/296).
|
||||||
|
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||||
|
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release in PyOpenSSL backend (https://github.com/ansible-collections/community.crypto/pull/300).
|
||||||
|
|
||||||
|
v1.9.4
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Regular bugfix release.
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- acme_* modules - fix commands composed for OpenSSL backend to retrieve information on CSRs and certificates from stdin to use ``/dev/stdin`` instead of ``-``. This is needed for OpenSSL 1.0.1 and 1.0.2, apparently (https://github.com/ansible-collections/community.crypto/pull/279).
|
||||||
|
- acme_challenge_cert_helper - only return exception when cryptography is not installed, not when a too old version of it is installed. This prevents Ansible's callback to crash (https://github.com/ansible-collections/community.crypto/pull/281).
|
||||||
|
|
||||||
|
v1.9.3
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Regular bugfix release.
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- openssl_csr and openssl_csr_pipe - make sure that Unicode strings are used to compare strings with the cryptography backend. This fixes idempotency problems with non-ASCII letters on Python 2 (https://github.com/ansible-collections/community.crypto/issues/270, https://github.com/ansible-collections/community.crypto/pull/271).
|
||||||
|
|
||||||
|
v1.9.2
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Bugfix release to fix the changelog. No other change compared to 1.9.0.
|
||||||
|
|
||||||
|
v1.9.1
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Accidental 1.9.1 release. Identical to 1.9.0.
|
||||||
|
|
||||||
|
v1.9.0
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Regular feature release.
|
||||||
|
|
||||||
|
Minor Changes
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- get_certificate - added ``starttls`` option to retrieve certificates from servers which require clients to request an encrypted connection (https://github.com/ansible-collections/community.crypto/pull/264).
|
||||||
|
- openssh_keypair - added ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- keypair_backend module utils - simplify code to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||||
|
- openssh_keypair - fixed ``cryptography`` backend to preserve original file permissions when regenerating a keypair requires existing files to be overwritten (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||||
|
- openssh_keypair - fixed error handling to restore original keypair if regeneration fails (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||||
|
- x509_crl - restore inherited function signature to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||||
|
|
||||||
|
v1.8.0
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Regular bugfix and feature release.
|
||||||
|
|
||||||
|
Minor Changes
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- Avoid internal ansible-core module_utils in favor of equivalent public API available since at least Ansible 2.9 (https://github.com/ansible-collections/community.crypto/pull/253).
|
||||||
|
- openssh certificate module utils - new module_utils for parsing OpenSSH certificates (https://github.com/ansible-collections/community.crypto/pull/246).
|
||||||
|
- openssh_cert - added ``regenerate`` option to validate additional certificate parameters which trigger regeneration of an existing certificate (https://github.com/ansible-collections/community.crypto/pull/256).
|
||||||
|
- openssh_cert - adding ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- openssh_cert - fixed certificate generation to restore original certificate if an error is encountered (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||||
|
- openssh_keypair - fixed a bug that prevented custom file attributes being applied to public keys (https://github.com/ansible-collections/community.crypto/pull/257).
|
||||||
|
|
||||||
|
v1.7.1
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Bugfix release.
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- openssl_pkcs12 - fix crash when loading passphrase-protected PKCS#12 files with ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/issues/247, https://github.com/ansible-collections/community.crypto/pull/248).
|
||||||
|
|
||||||
|
v1.7.0
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Regular feature and bugfix release.
|
||||||
|
|
||||||
|
Minor Changes
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- cryptography_openssh module utils - new module_utils for managing asymmetric keypairs and OpenSSH formatted/encoded asymmetric keypairs (https://github.com/ansible-collections/community.crypto/pull/213).
|
||||||
|
- openssh_keypair - added ``backend`` parameter for selecting between the cryptography library or the OpenSSH binary for the execution of actions performed by ``openssh_keypair`` (https://github.com/ansible-collections/community.crypto/pull/236).
|
||||||
|
- openssh_keypair - added ``passphrase`` parameter for encrypting/decrypting OpenSSH private keys (https://github.com/ansible-collections/community.crypto/pull/225).
|
||||||
|
- openssl_csr - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- openssl_csr_info - now returns ``public_key_type`` and ``public_key_data`` (https://github.com/ansible-collections/community.crypto/pull/233).
|
||||||
|
- openssl_csr_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/204).
|
||||||
|
- openssl_csr_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- openssl_pkcs12 - added option ``select_crypto_backend`` and a ``cryptography`` backend. This requires cryptography 3.0 or newer, and does not support the ``iter_size`` and ``maciter_size`` options (https://github.com/ansible-collections/community.crypto/pull/234).
|
||||||
|
- openssl_privatekey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- openssl_privatekey_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/205).
|
||||||
|
- openssl_privatekey_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- openssl_publickey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- x509_certificate - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- x509_certificate_info - now returns ``public_key_type`` and ``public_key_data`` (https://github.com/ansible-collections/community.crypto/pull/233).
|
||||||
|
- x509_certificate_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/206).
|
||||||
|
- x509_certificate_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- x509_crl - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- x509_crl_info - add ``list_revoked_certificates`` option to avoid enumerating all revoked certificates (https://github.com/ansible-collections/community.crypto/pull/232).
|
||||||
|
- x509_crl_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/203).
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- openssh_keypair - fix ``check_mode`` to populate return values for existing keypairs (https://github.com/ansible-collections/community.crypto/issues/113, https://github.com/ansible-collections/community.crypto/pull/230).
|
||||||
|
- various modules - prevent crashes when modules try to set attributes on not yet existing files in check mode. This will be fixed in ansible-core 2.12, but it is not backported to every Ansible version we support (https://github.com/ansible-collections/community.crypto/issue/242, https://github.com/ansible-collections/community.crypto/pull/243).
|
||||||
|
- x509_certificate - fix crash when ``assertonly`` provider is used and some error conditions should be reported (https://github.com/ansible-collections/community.crypto/issues/240, https://github.com/ansible-collections/community.crypto/pull/241).
|
||||||
|
|
||||||
|
New Modules
|
||||||
|
-----------
|
||||||
|
|
||||||
|
- openssl_publickey_info - Provide information for OpenSSL public keys
|
||||||
|
|
||||||
|
v1.6.2
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Bugfix release. Fixes compatibility issue of ACME modules with step-ca.
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- acme_* modules - avoid crashing for ACME servers where the ``meta`` directory key is not present (https://github.com/ansible-collections/community.crypto/issues/220, https://github.com/ansible-collections/community.crypto/pull/221).
|
||||||
|
|
||||||
|
v1.6.1
|
||||||
|
======
|
||||||
|
|
||||||
|
Release Summary
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Bugfix release.
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- acme_* modules - fix wrong usages of ``ACMEProtocolException`` (https://github.com/ansible-collections/community.crypto/pull/216, https://github.com/ansible-collections/community.crypto/pull/217).
|
||||||
|
|
||||||
v1.6.0
|
v1.6.0
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ Provides modules for [Ansible](https://www.ansible.com/community) for various cr
|
|||||||
|
|
||||||
You can find [documentation for this collection on the Ansible docs site](https://docs.ansible.com/ansible/latest/collections/community/crypto/).
|
You can find [documentation for this collection on the Ansible docs site](https://docs.ansible.com/ansible/latest/collections/community/crypto/).
|
||||||
|
|
||||||
|
Please note that this collection does **not** support Windows targets.
|
||||||
|
|
||||||
## Tested with Ansible
|
## Tested with Ansible
|
||||||
|
|
||||||
Tested with both the current Ansible 2.9 and 2.10 releases and the current development version of Ansible. Ansible versions before 2.9.10 are not supported.
|
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
|
## External requirements
|
||||||
|
|
||||||
|
|||||||
@@ -372,3 +372,253 @@ releases:
|
|||||||
- 202-actionmodule-plugin-utils-ansible-core-2.11.yml
|
- 202-actionmodule-plugin-utils-ansible-core-2.11.yml
|
||||||
- 207-acme-account-key-passphrase.yml
|
- 207-acme-account-key-passphrase.yml
|
||||||
release_date: '2021-03-22'
|
release_date: '2021-03-22'
|
||||||
|
1.6.1:
|
||||||
|
changes:
|
||||||
|
bugfixes:
|
||||||
|
- acme_* modules - fix wrong usages of ``ACMEProtocolException`` (https://github.com/ansible-collections/community.crypto/pull/216,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/217).
|
||||||
|
release_summary: Bugfix release.
|
||||||
|
fragments:
|
||||||
|
- 1.6.1.yml
|
||||||
|
- 217-acme-exceptions.yml
|
||||||
|
release_date: '2021-04-11'
|
||||||
|
1.6.2:
|
||||||
|
changes:
|
||||||
|
bugfixes:
|
||||||
|
- acme_* modules - avoid crashing for ACME servers where the ``meta`` directory
|
||||||
|
key is not present (https://github.com/ansible-collections/community.crypto/issues/220,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/221).
|
||||||
|
release_summary: Bugfix release. Fixes compatibility issue of ACME modules with
|
||||||
|
step-ca.
|
||||||
|
fragments:
|
||||||
|
- 1.6.2.yml
|
||||||
|
- 221-acme-meta.yml
|
||||||
|
release_date: '2021-04-28'
|
||||||
|
1.7.0:
|
||||||
|
changes:
|
||||||
|
bugfixes:
|
||||||
|
- openssh_keypair - fix ``check_mode`` to populate return values for existing
|
||||||
|
keypairs (https://github.com/ansible-collections/community.crypto/issues/113,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/230).
|
||||||
|
- various modules - prevent crashes when modules try to set attributes on not
|
||||||
|
yet existing files in check mode. This will be fixed in ansible-core 2.12,
|
||||||
|
but it is not backported to every Ansible version we support (https://github.com/ansible-collections/community.crypto/issue/242,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/243).
|
||||||
|
- x509_certificate - fix crash when ``assertonly`` provider is used and some
|
||||||
|
error conditions should be reported (https://github.com/ansible-collections/community.crypto/issues/240,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/241).
|
||||||
|
minor_changes:
|
||||||
|
- cryptography_openssh module utils - new module_utils for managing asymmetric
|
||||||
|
keypairs and OpenSSH formatted/encoded asymmetric keypairs (https://github.com/ansible-collections/community.crypto/pull/213).
|
||||||
|
- openssh_keypair - added ``backend`` parameter for selecting between the cryptography
|
||||||
|
library or the OpenSSH binary for the execution of actions performed by ``openssh_keypair``
|
||||||
|
(https://github.com/ansible-collections/community.crypto/pull/236).
|
||||||
|
- openssh_keypair - added ``passphrase`` parameter for encrypting/decrypting
|
||||||
|
OpenSSH private keys (https://github.com/ansible-collections/community.crypto/pull/225).
|
||||||
|
- openssl_csr - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- openssl_csr_info - now returns ``public_key_type`` and ``public_key_data``
|
||||||
|
(https://github.com/ansible-collections/community.crypto/pull/233).
|
||||||
|
- openssl_csr_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/204).
|
||||||
|
- openssl_csr_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- openssl_pkcs12 - added option ``select_crypto_backend`` and a ``cryptography``
|
||||||
|
backend. This requires cryptography 3.0 or newer, and does not support the
|
||||||
|
``iter_size`` and ``maciter_size`` options (https://github.com/ansible-collections/community.crypto/pull/234).
|
||||||
|
- openssl_privatekey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- openssl_privatekey_info - refactor module to allow code re-use for diff mode
|
||||||
|
(https://github.com/ansible-collections/community.crypto/pull/205).
|
||||||
|
- openssl_privatekey_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- openssl_publickey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- x509_certificate - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- x509_certificate_info - now returns ``public_key_type`` and ``public_key_data``
|
||||||
|
(https://github.com/ansible-collections/community.crypto/pull/233).
|
||||||
|
- x509_certificate_info - refactor module to allow code re-use for diff mode
|
||||||
|
(https://github.com/ansible-collections/community.crypto/pull/206).
|
||||||
|
- x509_certificate_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- x509_crl - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/150).
|
||||||
|
- x509_crl_info - add ``list_revoked_certificates`` option to avoid enumerating
|
||||||
|
all revoked certificates (https://github.com/ansible-collections/community.crypto/pull/232).
|
||||||
|
- x509_crl_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/203).
|
||||||
|
release_summary: Regular feature and bugfix release.
|
||||||
|
fragments:
|
||||||
|
- 1.7.0.yml
|
||||||
|
- 150-diff.yml
|
||||||
|
- 203-x509_crl_info.yml
|
||||||
|
- 204-openssl_csr_info.yml
|
||||||
|
- 205-openssl_privatekey_info.yml
|
||||||
|
- 206-x509_certificate_info.yml
|
||||||
|
- 213-cryptography-openssh-module-utils.yml
|
||||||
|
- 225-openssh-keypair-passphrase.yml
|
||||||
|
- 230-openssh_keypair-check_mode-return-values.yml
|
||||||
|
- 232-x509_crl_info-list_revoked_certificates.yml
|
||||||
|
- 233-public-key-info.yml
|
||||||
|
- 234-openssl_pkcs12-cryptography.yml
|
||||||
|
- 236-openssh_keypair-backends.yml
|
||||||
|
- 241-x509_certificate-assertonly.yml
|
||||||
|
- 243-permission-check-crash.yml
|
||||||
|
modules:
|
||||||
|
- description: Provide information for OpenSSL public keys
|
||||||
|
name: openssl_publickey_info
|
||||||
|
namespace: ''
|
||||||
|
release_date: '2021-06-02'
|
||||||
|
1.7.1:
|
||||||
|
changes:
|
||||||
|
bugfixes:
|
||||||
|
- openssl_pkcs12 - fix crash when loading passphrase-protected PKCS#12 files
|
||||||
|
with ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/issues/247,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/248).
|
||||||
|
release_summary: Bugfix release.
|
||||||
|
fragments:
|
||||||
|
- 1.7.1.yml
|
||||||
|
- 248-openssl_pkcs12-passphrase-fix.yml
|
||||||
|
release_date: '2021-06-11'
|
||||||
|
1.8.0:
|
||||||
|
changes:
|
||||||
|
bugfixes:
|
||||||
|
- openssh_cert - fixed certificate generation to restore original certificate
|
||||||
|
if an error is encountered (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||||
|
- openssh_keypair - fixed a bug that prevented custom file attributes being
|
||||||
|
applied to public keys (https://github.com/ansible-collections/community.crypto/pull/257).
|
||||||
|
minor_changes:
|
||||||
|
- Avoid internal ansible-core module_utils in favor of equivalent public API
|
||||||
|
available since at least Ansible 2.9 (https://github.com/ansible-collections/community.crypto/pull/253).
|
||||||
|
- openssh certificate module utils - new module_utils for parsing OpenSSH certificates
|
||||||
|
(https://github.com/ansible-collections/community.crypto/pull/246).
|
||||||
|
- openssh_cert - added ``regenerate`` option to validate additional certificate
|
||||||
|
parameters which trigger regeneration of an existing certificate (https://github.com/ansible-collections/community.crypto/pull/256).
|
||||||
|
- openssh_cert - adding ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/255).
|
||||||
|
release_summary: Regular bugfix and feature release.
|
||||||
|
fragments:
|
||||||
|
- 1.8.0.yml
|
||||||
|
- 246-openssh-certificate-module-utils.yml
|
||||||
|
- 255-openssh_cert-adding-diff-support.yml
|
||||||
|
- 256-openssh_cert-adding-idempotency-option.yml
|
||||||
|
- 257-openssh-keypair-fix-pubkey-permissions.yml
|
||||||
|
- ansible-core-_text.yml
|
||||||
|
release_date: '2021-08-10'
|
||||||
|
1.9.0:
|
||||||
|
changes:
|
||||||
|
bugfixes:
|
||||||
|
- keypair_backend module utils - simplify code to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||||
|
- openssh_keypair - fixed ``cryptography`` backend to preserve original file
|
||||||
|
permissions when regenerating a keypair requires existing files to be overwritten
|
||||||
|
(https://github.com/ansible-collections/community.crypto/pull/260).
|
||||||
|
- openssh_keypair - fixed error handling to restore original keypair if regeneration
|
||||||
|
fails (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||||
|
- x509_crl - restore inherited function signature to pass sanity tests (https://github.com/ansible-collections/community.crypto/pull/263).
|
||||||
|
minor_changes:
|
||||||
|
- get_certificate - added ``starttls`` option to retrieve certificates from
|
||||||
|
servers which require clients to request an encrypted connection (https://github.com/ansible-collections/community.crypto/pull/264).
|
||||||
|
- openssh_keypair - added ``diff`` support (https://github.com/ansible-collections/community.crypto/pull/260).
|
||||||
|
release_summary: Regular feature release.
|
||||||
|
fragments:
|
||||||
|
- 1.9.0.yml
|
||||||
|
- 260-openssh_keypair-diff-support.yml
|
||||||
|
- 263-sanity.yml
|
||||||
|
- 264-get_certificate-add-starttls-option.yml
|
||||||
|
release_date: '2021-08-30'
|
||||||
|
1.9.1:
|
||||||
|
changes:
|
||||||
|
release_summary: Accidental 1.9.1 release. Identical to 1.9.0.
|
||||||
|
release_date: '2021-08-30'
|
||||||
|
1.9.2:
|
||||||
|
changes:
|
||||||
|
release_summary: Bugfix release to fix the changelog. No other change compared
|
||||||
|
to 1.9.0.
|
||||||
|
fragments:
|
||||||
|
- 1.9.2.yml
|
||||||
|
release_date: '2021-08-30'
|
||||||
|
1.9.3:
|
||||||
|
changes:
|
||||||
|
bugfixes:
|
||||||
|
- openssl_csr and openssl_csr_pipe - make sure that Unicode strings are used
|
||||||
|
to compare strings with the cryptography backend. This fixes idempotency problems
|
||||||
|
with non-ASCII letters on Python 2 (https://github.com/ansible-collections/community.crypto/issues/270,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/271).
|
||||||
|
release_summary: Regular bugfix release.
|
||||||
|
fragments:
|
||||||
|
- 1.9.3.yml
|
||||||
|
- 271-openssl_csr-utf8.yml
|
||||||
|
release_date: '2021-09-14'
|
||||||
|
1.9.4:
|
||||||
|
changes:
|
||||||
|
bugfixes:
|
||||||
|
- acme_* modules - fix commands composed for OpenSSL backend to retrieve information
|
||||||
|
on CSRs and certificates from stdin to use ``/dev/stdin`` instead of ``-``.
|
||||||
|
This is needed for OpenSSL 1.0.1 and 1.0.2, apparently (https://github.com/ansible-collections/community.crypto/pull/279).
|
||||||
|
- acme_challenge_cert_helper - only return exception when cryptography is not
|
||||||
|
installed, not when a too old version of it is installed. This prevents Ansible's
|
||||||
|
callback to crash (https://github.com/ansible-collections/community.crypto/pull/281).
|
||||||
|
release_summary: Regular bugfix release.
|
||||||
|
fragments:
|
||||||
|
- 1.9.4.yml
|
||||||
|
- 279-acme-openssl.yml
|
||||||
|
- 282-acme_challenge_cert_helper-error.yml
|
||||||
|
release_date: '2021-09-28'
|
||||||
|
1.9.5:
|
||||||
|
changes:
|
||||||
|
bugfixes:
|
||||||
|
- get_certificate - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
|
||||||
|
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release
|
||||||
|
(https://github.com/ansible-collections/community.crypto/pull/294).
|
||||||
|
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release
|
||||||
|
in PyOpenSSL backend (https://github.com/ansible-collections/community.crypto/pull/300).
|
||||||
|
- openssl_pkcs12 - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/296).
|
||||||
|
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release
|
||||||
|
(https://github.com/ansible-collections/community.crypto/pull/294).
|
||||||
|
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release
|
||||||
|
in PyOpenSSL backend (https://github.com/ansible-collections/community.crypto/pull/300).
|
||||||
|
release_summary: Bugfix release to fully support cryptography 35.0.0.
|
||||||
|
fragments:
|
||||||
|
- 1.9.5.yml
|
||||||
|
- 294-cryptography-35.0.0.yml
|
||||||
|
- 296-openssl_pkcs12-cryptography-35.yml
|
||||||
|
- 300-pyopenssl-cryptography-35.yml
|
||||||
|
release_date: '2021-10-06'
|
||||||
|
1.9.6:
|
||||||
|
changes:
|
||||||
|
bugfixes:
|
||||||
|
- cryptography backend - improve Unicode handling for Python 2 (https://github.com/ansible-collections/community.crypto/pull/313).
|
||||||
|
release_summary: Regular bugfix release.
|
||||||
|
fragments:
|
||||||
|
- 1.9.6.yml
|
||||||
|
- 313-unicode-names.yml
|
||||||
|
release_date: '2021-10-30'
|
||||||
|
1.9.7:
|
||||||
|
changes:
|
||||||
|
bugfixes:
|
||||||
|
- acme_certificate - avoid passing multiple certificates to ``cryptography``'s
|
||||||
|
X.509 certificate loader when ``fullchain_dest`` is used (https://github.com/ansible-collections/community.crypto/pull/324).
|
||||||
|
- get_certificate, openssl_csr_info, x509_certificate_info - add fallback code
|
||||||
|
for extension parsing that works with cryptography 36.0.0 and newer. This
|
||||||
|
code re-serializes de-serialized extensions and thus can return slightly different
|
||||||
|
values if the extension in the original CSR resp. certificate was not canonicalized
|
||||||
|
correctly. This code is currently used as a fallback if the existing code
|
||||||
|
stops working, but we will switch it to be the main code in a future release
|
||||||
|
(https://github.com/ansible-collections/community.crypto/pull/331).
|
||||||
|
- luks_device - now also runs a built-in LUKS signature cleaner on ``state=absent``
|
||||||
|
to make sure that also the secondary LUKS2 header is wiped when older versions
|
||||||
|
of wipefs are used (https://github.com/ansible-collections/community.crypto/issues/326,
|
||||||
|
https://github.com/ansible-collections/community.crypto/pull/327).
|
||||||
|
- openssl_pkcs12 - use new PKCS#12 deserialization infrastructure from cryptography
|
||||||
|
36.0.0 if available (https://github.com/ansible-collections/community.crypto/pull/302).
|
||||||
|
minor_changes:
|
||||||
|
- acme_* modules - fix usage of ``fetch_url`` with changes in latest ansible-core
|
||||||
|
``devel`` branch (https://github.com/ansible-collections/community.crypto/pull/339).
|
||||||
|
release_summary: Bugfix release with extra forward compatibility for newer versions
|
||||||
|
of cryptography.
|
||||||
|
fragments:
|
||||||
|
- 1.9.7.yml
|
||||||
|
- 302-openssl_pkcs12-cryptography-36.0.0.yml
|
||||||
|
- 324-acme_certificate-fullchain.yml
|
||||||
|
- 327-luks_device-wipe.yml
|
||||||
|
- 331-cryptography-extensions.yml
|
||||||
|
- fetch_url-devel.yml
|
||||||
|
release_date: '2021-11-22'
|
||||||
|
|||||||
6
docs/docsite/extra-docs.yml
Normal file
6
docs/docsite/extra-docs.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
sections:
|
||||||
|
- title: Scenario Guides
|
||||||
|
toctree:
|
||||||
|
- guide_selfsigned
|
||||||
|
- guide_ownca
|
||||||
148
docs/docsite/rst/guide_ownca.rst
Normal file
148
docs/docsite/rst/guide_ownca.rst
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
.. _ansible_collections.community.crypto.docsite.guide_ownca:
|
||||||
|
|
||||||
|
How to create a small CA
|
||||||
|
========================
|
||||||
|
|
||||||
|
The `community.crypto collection <https://galaxy.ansible.com/community/crypto>`_ offers multiple modules that create private keys, certificate signing requests, and certificates. This guide shows how to create your own small CA and how to use it to sign certificates.
|
||||||
|
|
||||||
|
In all examples, we assume that the CA's private key is password protected, where the password is provided in the ``secret_ca_passphrase`` variable.
|
||||||
|
|
||||||
|
Set up the CA
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any certificate can be used as a CA certificate. You can create a self-signed certificate (see :ref:`ansible_collections.community.crypto.docsite.guide_selfsigned`), use another CA certificate to sign a new certificate (using the instructions below for signing a certificate), ask (and pay) a commercial CA to sign your CA certificate, etc.
|
||||||
|
|
||||||
|
The following instructions show how to set up a simple self-signed CA certificate.
|
||||||
|
|
||||||
|
.. code-block:: yaml+jinja
|
||||||
|
|
||||||
|
- name: Create private key with password protection
|
||||||
|
community.crypto.openssl_privatekey:
|
||||||
|
path: /path/to/ca-certificate.key
|
||||||
|
passphrase: "{{ secret_ca_passphrase }}"
|
||||||
|
|
||||||
|
- name: Create certificate signing request (CSR) for CA certificate
|
||||||
|
community.crypto.openssl_csr_pipe:
|
||||||
|
privatekey_path: /path/to/ca-certificate.key
|
||||||
|
privatekey_passphrase: "{{ secret_ca_passphrase }}"
|
||||||
|
common_name: Ansible CA
|
||||||
|
use_common_name_for_san: false # since we do not specify SANs, don't use CN as a SAN
|
||||||
|
basic_constraints:
|
||||||
|
- 'CA:TRUE'
|
||||||
|
basic_constraints_critical: yes
|
||||||
|
key_usage:
|
||||||
|
- keyCertSign
|
||||||
|
key_usage_critical: true
|
||||||
|
register: ca_csr
|
||||||
|
|
||||||
|
- name: Create self-signed CA certificate from CSR
|
||||||
|
community.crypto.x509_certificate:
|
||||||
|
path: /path/to/ca-certificate.pem
|
||||||
|
csr_content: "{{ ca_csr.csr }}"
|
||||||
|
privatekey_path: /path/to/ca-certificate.key
|
||||||
|
privatekey_passphrase: "{{ secret_ca_passphrase }}"
|
||||||
|
provider: selfsigned
|
||||||
|
|
||||||
|
Use the CA to sign a certificate
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
To sign a certificate, you must pass a CSR to the :ref:`community.crypto.x509_certificate module <ansible_collections.community.crypto.x509_certificate_module>` or :ref:`community.crypto.x509_certificate_pipe module <ansible_collections.community.crypto.x509_certificate_pipe_module>`.
|
||||||
|
|
||||||
|
In the following example, we assume that the certificate to sign (including its private key) are on ``server_1``, while our CA certificate is on ``server_2``. We do not want any key material to leave each respective server.
|
||||||
|
|
||||||
|
.. code-block:: yaml+jinja
|
||||||
|
|
||||||
|
- name: Create private key for new certificate on server_1
|
||||||
|
community.crypto.openssl_privatekey:
|
||||||
|
path: /path/to/certificate.key
|
||||||
|
delegate_to: server_1
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
- name: Create certificate signing request (CSR) for new certificate
|
||||||
|
community.crypto.openssl_csr_pipe:
|
||||||
|
privatekey_path: /path/to/certificate.key
|
||||||
|
subject_alt_name:
|
||||||
|
- "DNS:ansible.com"
|
||||||
|
- "DNS:www.ansible.com"
|
||||||
|
- "DNS:docs.ansible.com"
|
||||||
|
delegate_to: server_1
|
||||||
|
run_once: true
|
||||||
|
register: csr
|
||||||
|
|
||||||
|
- name: Sign certificate with our CA
|
||||||
|
community.crypto.x509_certificate_pipe:
|
||||||
|
csr_content: "{{ ca_csr.csr }}"
|
||||||
|
provider: ownca
|
||||||
|
ownca_path: /path/to/ca-certificate.pem
|
||||||
|
ownca_privatekey_path: /path/to/ca-certificate.key
|
||||||
|
ownca_privatekey_passphrase: "{{ secret_ca_passphrase }}"
|
||||||
|
ownca_not_after: +365d # valid for one year
|
||||||
|
ownca_not_before: "-1d" # valid since yesterday
|
||||||
|
delegate_to: server_2
|
||||||
|
run_once: true
|
||||||
|
register: certificate
|
||||||
|
|
||||||
|
- name: Write certificate file on server_1
|
||||||
|
copy:
|
||||||
|
dest: /path/to/certificate.pem
|
||||||
|
content: "{{ certificate.certificate }}"
|
||||||
|
delegate_to: server_1
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
Please note that the above procedure is **not idempotent**. The following extended example reads the existing certificate from ``server_1`` (if exists) and provides it to the :ref:`community.crypto.x509_certificate_pipe module <ansible_collections.community.crypto.x509_certificate_pipe_module>`, and only writes the result back if it was changed:
|
||||||
|
|
||||||
|
.. code-block:: yaml+jinja
|
||||||
|
|
||||||
|
- name: Create private key for new certificate on server_1
|
||||||
|
community.crypto.openssl_privatekey:
|
||||||
|
path: /path/to/certificate.key
|
||||||
|
delegate_to: server_1
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
- name: Create certificate signing request (CSR) for new certificate
|
||||||
|
community.crypto.openssl_csr_pipe:
|
||||||
|
privatekey_path: /path/to/certificate.key
|
||||||
|
subject_alt_name:
|
||||||
|
- "DNS:ansible.com"
|
||||||
|
- "DNS:www.ansible.com"
|
||||||
|
- "DNS:docs.ansible.com"
|
||||||
|
delegate_to: server_1
|
||||||
|
run_once: true
|
||||||
|
register: csr
|
||||||
|
|
||||||
|
- name: Check whether certificate exists
|
||||||
|
stat:
|
||||||
|
path: /path/to/certificate.pem
|
||||||
|
delegate_to: server_1
|
||||||
|
run_once: true
|
||||||
|
register: certificate_exists
|
||||||
|
|
||||||
|
- name: Read existing certificate if exists
|
||||||
|
slurp:
|
||||||
|
src: /path/to/certificate.pem
|
||||||
|
when: certificate_exists.stat.exists
|
||||||
|
delegate_to: server_1
|
||||||
|
run_once: true
|
||||||
|
register: certificate
|
||||||
|
|
||||||
|
- name: Sign certificate with our CA
|
||||||
|
community.crypto.x509_certificate_pipe:
|
||||||
|
content: "{{ (certificate.content | b64decode) if certificate_exists.stat.exists else omit }}"
|
||||||
|
csr_content: "{{ ca_csr.csr }}"
|
||||||
|
provider: ownca
|
||||||
|
ownca_path: /path/to/ca-certificate.pem
|
||||||
|
ownca_privatekey_path: /path/to/ca-certificate.key
|
||||||
|
ownca_privatekey_passphrase: "{{ secret_ca_passphrase }}"
|
||||||
|
ownca_not_after: +365d # valid for one year
|
||||||
|
ownca_not_before: "-1d" # valid since yesterday
|
||||||
|
delegate_to: server_2
|
||||||
|
run_once: true
|
||||||
|
register: certificate
|
||||||
|
|
||||||
|
- name: Write certificate file on server_1
|
||||||
|
copy:
|
||||||
|
dest: /path/to/certificate.pem
|
||||||
|
content: "{{ certificate.certificate }}"
|
||||||
|
delegate_to: server_1
|
||||||
|
run_once: true
|
||||||
|
when: certificate is changed
|
||||||
60
docs/docsite/rst/guide_selfsigned.rst
Normal file
60
docs/docsite/rst/guide_selfsigned.rst
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
.. _ansible_collections.community.crypto.docsite.guide_selfsigned:
|
||||||
|
|
||||||
|
How to create self-signed certificates
|
||||||
|
======================================
|
||||||
|
|
||||||
|
The `community.crypto collection <https://galaxy.ansible.com/community/crypto>`_ offers multiple modules that create private keys, certificate signing requests, and certificates. This guide shows how to create self-signed certificates.
|
||||||
|
|
||||||
|
For creating any kind of certificate, you always have to start with a private key. You can use the :ref:`community.crypto.openssl_privatekey module <ansible_collections.community.crypto.openssl_privatekey_module>` to create a private key. If you only specify ``path``, the default parameters will be used. This will result in a 4096 bit RSA private key:
|
||||||
|
|
||||||
|
.. code-block:: yaml+jinja
|
||||||
|
|
||||||
|
- name: Create private key (RSA, 4096 bits)
|
||||||
|
community.crypto.openssl_privatekey:
|
||||||
|
path: /path/to/certificate.key
|
||||||
|
|
||||||
|
You can specify ``type`` to select another key type, ``size`` to select a different key size (only available for RSA and DSA keys), or ``passphrase`` if you want to store the key password-protected:
|
||||||
|
|
||||||
|
.. code-block:: yaml+jinja
|
||||||
|
|
||||||
|
- name: Create private key (X25519) with password protection
|
||||||
|
community.crypto.openssl_privatekey:
|
||||||
|
path: /path/to/certificate.key
|
||||||
|
type: X25519
|
||||||
|
passphrase: changeme
|
||||||
|
|
||||||
|
To create a very simple self-signed certificate with no specific information, you can proceed directly with the :ref:`community.crypto.x509_certificate module <ansible_collections.community.crypto.x509_certificate_module>`:
|
||||||
|
|
||||||
|
.. code-block:: yaml+jinja
|
||||||
|
|
||||||
|
- name: Create simple self-signed certificate
|
||||||
|
community.crypto.x509_certificate:
|
||||||
|
path: /path/to/certificate.pem
|
||||||
|
privatekey_path: /path/to/certificate.key
|
||||||
|
provider: selfsigned
|
||||||
|
|
||||||
|
(If you used ``passphrase`` for the private key, you have to provide ``privatekey_passphrase``.)
|
||||||
|
|
||||||
|
You can use ``selfsigned_not_after`` to define when the certificate expires (default: in roughly 10 years), and ``selfsigned_not_before`` to define from when the certificate is valid (default: now).
|
||||||
|
|
||||||
|
To define further properties of the certificate, like the subject, Subject Alternative Names (SANs), key usages, name constraints, etc., you need to first create a Certificate Signing Request (CSR) and provide it to the :ref:`community.crypto.x509_certificate module <ansible_collections.community.crypto.x509_certificate_module>`. If you do not need the CSR file, you can use the :ref:`community.crypto.openssl_csr_pipe module <ansible_collections.community.crypto.openssl_csr_pipe_module>` as in the example below. (To store it to disk, use the :ref:`community.crypto.openssl_csr module <ansible_collections.community.crypto.openssl_csr_module>` instead.)
|
||||||
|
|
||||||
|
.. code-block:: yaml+jinja
|
||||||
|
|
||||||
|
- name: Create certificate signing request (CSR) for self-signed certificate
|
||||||
|
community.crypto.openssl_csr_pipe:
|
||||||
|
privatekey_path: /path/to/certificate.key
|
||||||
|
common_name: ansible.com
|
||||||
|
organization_name: Ansible, Inc.
|
||||||
|
subject_alt_name:
|
||||||
|
- "DNS:ansible.com"
|
||||||
|
- "DNS:www.ansible.com"
|
||||||
|
- "DNS:docs.ansible.com"
|
||||||
|
register: csr
|
||||||
|
|
||||||
|
- name: Create self-signed certificate from CSR
|
||||||
|
community.crypto.x509_certificate:
|
||||||
|
path: /path/to/certificate.pem
|
||||||
|
csr_content: "{{ csr.csr }}"
|
||||||
|
privatekey_path: /path/to/certificate.key
|
||||||
|
provider: selfsigned
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace: community
|
namespace: community
|
||||||
name: crypto
|
name: crypto
|
||||||
version: 1.6.0
|
version: 1.9.7
|
||||||
readme: README.md
|
readme: README.md
|
||||||
authors:
|
authors:
|
||||||
- Ansible (github.com/ansible)
|
- Ansible (github.com/ansible)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ __metaclass__ = type
|
|||||||
|
|
||||||
import base64
|
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
|
from ansible_collections.community.crypto.plugins.plugin_utils.action_module import ActionModuleBase
|
||||||
|
|
||||||
|
|||||||
@@ -457,8 +457,8 @@ options:
|
|||||||
- Time will always be interpreted as UTC.
|
- Time will always be interpreted as UTC.
|
||||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
+ 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.
|
- If this value is not specified, the certificate will start being valid from now.
|
||||||
|
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||||
- This is only used by the C(ownca) provider.
|
- This is only used by the C(ownca) provider.
|
||||||
type: str
|
type: str
|
||||||
default: +0s
|
default: +0s
|
||||||
@@ -470,8 +470,8 @@ options:
|
|||||||
- Time will always be interpreted as UTC.
|
- Time will always be interpreted as UTC.
|
||||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
+ 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.
|
- If this value is not specified, the certificate will stop being valid 10 years from now.
|
||||||
|
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||||
- This is only used by the C(ownca) provider.
|
- 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.
|
- 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.
|
Please see U(https://support.apple.com/en-us/HT210176) for more details.
|
||||||
@@ -548,8 +548,8 @@ options:
|
|||||||
- Time will always be interpreted as UTC.
|
- Time will always be interpreted as UTC.
|
||||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
+ 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.
|
- If this value is not specified, the certificate will start being valid from now.
|
||||||
|
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||||
- This is only used by the C(selfsigned) provider.
|
- This is only used by the C(selfsigned) provider.
|
||||||
type: str
|
type: str
|
||||||
default: +0s
|
default: +0s
|
||||||
@@ -562,8 +562,8 @@ options:
|
|||||||
- Time will always be interpreted as UTC.
|
- Time will always be interpreted as UTC.
|
||||||
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
|
||||||
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
|
+ 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.
|
- If this value is not specified, the certificate will stop being valid 10 years from now.
|
||||||
|
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
|
||||||
- This is only used by the C(selfsigned) provider.
|
- 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.
|
- 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.
|
Please see U(https://support.apple.com/en-us/HT210176) for more details.
|
||||||
|
|||||||
@@ -227,12 +227,11 @@ options:
|
|||||||
description:
|
description:
|
||||||
- The authority key identifier as a hex string, where two bytes are separated by colons.
|
- 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)"
|
- "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
|
- "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
|
own choice. Specifying this option is mostly useful for self-signed certificates
|
||||||
or for own CAs."
|
or for own CAs."
|
||||||
- Note that this is only supported if the C(cryptography) backend is used!
|
- 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.
|
I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
|
||||||
type: str
|
type: str
|
||||||
authority_cert_issuer:
|
authority_cert_issuer:
|
||||||
@@ -241,23 +240,24 @@ options:
|
|||||||
- Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
|
- 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)
|
C(otherName) and the ones specific to your CA)
|
||||||
- "Example: C(DNS:ca.example.org)"
|
- "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
|
- "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
|
own choice. Specifying this option is mostly useful for self-signed certificates
|
||||||
or for own CAs."
|
or for own CAs."
|
||||||
- Note that this is only supported if the C(cryptography) backend is used!
|
- 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.
|
I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
elements: str
|
||||||
authority_cert_serial_number:
|
authority_cert_serial_number:
|
||||||
description:
|
description:
|
||||||
- The authority cert serial number.
|
- 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!
|
- 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
|
- "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
|
own choice. Specifying this option is mostly useful for self-signed certificates
|
||||||
or for own CAs."
|
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.
|
I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
|
||||||
type: int
|
type: int
|
||||||
crl_distribution_points:
|
crl_distribution_points:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import traceback
|
|||||||
from ansible.module_utils.basic import missing_required_lib
|
from ansible.module_utils.basic import missing_required_lib
|
||||||
from ansible.module_utils.urls import fetch_url
|
from ansible.module_utils.urls import fetch_url
|
||||||
from ansible.module_utils.six.moves.urllib.parse import unquote
|
from ansible.module_utils.six.moves.urllib.parse import unquote
|
||||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||||
get_default_argspec,
|
get_default_argspec,
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ import json
|
|||||||
import locale
|
import locale
|
||||||
|
|
||||||
from ansible.module_utils.basic import missing_required_lib
|
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.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 (
|
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
|
||||||
OpenSSLCLIBackend,
|
OpenSSLCLIBackend,
|
||||||
@@ -82,6 +83,9 @@ class ACMEDirectory(object):
|
|||||||
for key in ('newNonce', 'newAccount', 'newOrder'):
|
for key in ('newNonce', 'newAccount', 'newOrder'):
|
||||||
if key not in self.directory:
|
if key not in self.directory:
|
||||||
raise ModuleFailException("ACME directory does not seem to follow protocol ACME v2")
|
raise ModuleFailException("ACME directory does not seem to follow protocol ACME v2")
|
||||||
|
# Make sure that 'meta' is always available
|
||||||
|
if 'meta' not in self.directory:
|
||||||
|
self.directory['meta'] = {}
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return self.directory[key]
|
return self.directory[key]
|
||||||
@@ -225,9 +229,14 @@ class ACMEClient(object):
|
|||||||
resp, info = fetch_url(self.module, url, data=data, headers=headers, method='POST')
|
resp, info = fetch_url(self.module, url, data=data, headers=headers, method='POST')
|
||||||
_assert_fetch_url_success(self.module, resp, info)
|
_assert_fetch_url_success(self.module, resp, info)
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
try:
|
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()
|
content = resp.read()
|
||||||
except AttributeError:
|
except (AttributeError, TypeError):
|
||||||
content = info.pop('body', None)
|
content = info.pop('body', None)
|
||||||
|
|
||||||
if content or not parse_json_result:
|
if content or not parse_json_result:
|
||||||
@@ -281,17 +290,23 @@ class ACMEClient(object):
|
|||||||
_assert_fetch_url_success(self.module, resp, info)
|
_assert_fetch_url_success(self.module, resp, info)
|
||||||
|
|
||||||
try:
|
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()
|
content = resp.read()
|
||||||
except AttributeError:
|
except (AttributeError, TypeError):
|
||||||
content = info.pop('body', None)
|
content = info.pop('body', None)
|
||||||
|
|
||||||
# Process result
|
# Process result
|
||||||
|
parsed_json_result = False
|
||||||
if parse_json_result:
|
if parse_json_result:
|
||||||
result = {}
|
result = {}
|
||||||
if content:
|
if content:
|
||||||
if info['content-type'].startswith('application/json'):
|
if info['content-type'].startswith('application/json'):
|
||||||
try:
|
try:
|
||||||
result = self.module.from_json(content.decode('utf8'))
|
result = self.module.from_json(content.decode('utf8'))
|
||||||
|
parsed_json_result = True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise NetworkException("Failed to parse the ACME response: {0} {1}".format(uri, content))
|
raise NetworkException("Failed to parse the ACME response: {0} {1}".format(uri, content))
|
||||||
else:
|
else:
|
||||||
@@ -301,7 +316,7 @@ class ACMEClient(object):
|
|||||||
|
|
||||||
if fail_on_error and _is_failed(info, expected_status_codes=expected_status_codes):
|
if fail_on_error and _is_failed(info, expected_status_codes=expected_status_codes):
|
||||||
raise ACMEProtocolException(
|
raise ACMEProtocolException(
|
||||||
self.module, msg=error_msg, info=info, content=content, content_json=result if parse_json_result else None)
|
self.module, msg=error_msg, info=info, content=content, content_json=result if parsed_json_result else None)
|
||||||
return result, info
|
return result, info
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import datetime
|
|||||||
import os
|
import os
|
||||||
import sys
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
|
||||||
CryptoBackend,
|
CryptoBackend,
|
||||||
@@ -41,6 +41,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
|||||||
cryptography_name_to_oid,
|
cryptography_name_to_oid,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||||
|
extract_first_pem,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cryptography
|
import cryptography
|
||||||
import cryptography.hazmat.backends
|
import cryptography.hazmat.backends
|
||||||
@@ -357,6 +361,9 @@ class CryptographyBackend(CryptoBackend):
|
|||||||
if cert_content is None:
|
if cert_content is None:
|
||||||
return -1
|
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:
|
try:
|
||||||
cert = cryptography.x509.load_pem_x509_certificate(cert_content, _cryptography_backend)
|
cert = cryptography.x509.load_pem_x509_certificate(cert_content, _cryptography_backend)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import re
|
|||||||
import tempfile
|
import tempfile
|
||||||
import traceback
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
|
||||||
CryptoBackend,
|
CryptoBackend,
|
||||||
@@ -230,7 +230,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
|||||||
filename = csr_filename
|
filename = csr_filename
|
||||||
data = None
|
data = None
|
||||||
if csr_content is not None:
|
if csr_content is not None:
|
||||||
filename = '-'
|
filename = '/dev/stdin'
|
||||||
data = csr_content.encode('utf-8')
|
data = csr_content.encode('utf-8')
|
||||||
|
|
||||||
openssl_csr_cmd = [self.openssl_binary, "req", "-in", filename, "-noout", "-text"]
|
openssl_csr_cmd = [self.openssl_binary, "req", "-in", filename, "-noout", "-text"]
|
||||||
@@ -267,7 +267,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
|||||||
filename = cert_filename
|
filename = cert_filename
|
||||||
data = None
|
data = None
|
||||||
if cert_content is not None:
|
if cert_content is not None:
|
||||||
filename = '-'
|
filename = '/dev/stdin'
|
||||||
data = cert_content.encode('utf-8')
|
data = cert_content.encode('utf-8')
|
||||||
cert_filename_suffix = ''
|
cert_filename_suffix = ''
|
||||||
elif cert_filename is not None:
|
elif cert_filename is not None:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import json
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from ansible.module_utils._text import to_bytes
|
from ansible.module_utils.common.text.converters import to_bytes
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ class Authorization(object):
|
|||||||
new_authz["resource"] = "new-authz"
|
new_authz["resource"] = "new-authz"
|
||||||
else:
|
else:
|
||||||
if 'newAuthz' not in client.directory.directory:
|
if 'newAuthz' not in client.directory.directory:
|
||||||
raise ACMEProtocolException('ACME endpoint does not support pre-authorization')
|
raise ACMEProtocolException(client.module, 'ACME endpoint does not support pre-authorization')
|
||||||
url = client.directory['newAuthz']
|
url = client.directory['newAuthz']
|
||||||
|
|
||||||
result, info = client.send_signed_request(
|
result, info = client.send_signed_request(
|
||||||
@@ -214,7 +214,7 @@ class Authorization(object):
|
|||||||
data[challenge.type] = validation_data
|
data[challenge.type] = validation_data
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def raise_error(self, error_msg):
|
def raise_error(self, error_msg, module=None):
|
||||||
'''
|
'''
|
||||||
Aborts with a specific error for a challenge.
|
Aborts with a specific error for a challenge.
|
||||||
'''
|
'''
|
||||||
@@ -227,17 +227,20 @@ class Authorization(object):
|
|||||||
if 'error' in challenge.data:
|
if 'error' in challenge.data:
|
||||||
msg = '{msg}: {problem}'.format(
|
msg = '{msg}: {problem}'.format(
|
||||||
msg=msg,
|
msg=msg,
|
||||||
problem=format_error_problem(challenge.data['error'], subproblem_prefix='{0}.'.format(type)),
|
problem=format_error_problem(challenge.data['error'], subproblem_prefix='{0}.'.format(challenge.type)),
|
||||||
)
|
)
|
||||||
error_details.append(msg)
|
error_details.append(msg)
|
||||||
raise ACMEProtocolException(
|
raise ACMEProtocolException(
|
||||||
|
module,
|
||||||
'Failed to validate challenge for {identifier}: {error}. {details}'.format(
|
'Failed to validate challenge for {identifier}: {error}. {details}'.format(
|
||||||
identifier=self.combined_identifier,
|
identifier=self.combined_identifier,
|
||||||
error=error_msg,
|
error=error_msg,
|
||||||
details='; '.join(error_details),
|
details='; '.join(error_details),
|
||||||
),
|
),
|
||||||
identifier=self.combined_identifier,
|
extras=dict(
|
||||||
authorization=self.data,
|
identifier=self.combined_identifier,
|
||||||
|
authorization=self.data,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def find_challenge(self, challenge_type):
|
def find_challenge(self, challenge_type):
|
||||||
@@ -254,7 +257,7 @@ class Authorization(object):
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
if self.status == 'invalid':
|
if self.status == 'invalid':
|
||||||
self.raise_error('Status is "invalid"')
|
self.raise_error('Status is "invalid"', module=client.module)
|
||||||
|
|
||||||
return self.status == 'valid'
|
return self.status == 'valid'
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
|
from ansible.module_utils.common.text.converters import to_text
|
||||||
|
from ansible.module_utils.six import binary_type, PY3
|
||||||
|
|
||||||
|
|
||||||
def format_error_problem(problem, subproblem_prefix=''):
|
def format_error_problem(problem, subproblem_prefix=''):
|
||||||
if 'title' in problem:
|
if 'title' in problem:
|
||||||
@@ -23,7 +26,7 @@ def format_error_problem(problem, subproblem_prefix=''):
|
|||||||
msg = '{msg} Subproblems:'.format(msg=msg)
|
msg = '{msg} Subproblems:'.format(msg=msg)
|
||||||
for index, problem in enumerate(subproblems):
|
for index, problem in enumerate(subproblems):
|
||||||
index_str = '{prefix}{index}'.format(prefix=subproblem_prefix, index=index)
|
index_str = '{prefix}{index}'.format(prefix=subproblem_prefix, index=index)
|
||||||
msg = '{msg}\n({index}) {problem}.'.format(
|
msg = '{msg}\n({index}) {problem}'.format(
|
||||||
msg=msg,
|
msg=msg,
|
||||||
index=index_str,
|
index=index_str,
|
||||||
problem=format_error_problem(problem, subproblem_prefix='{0}.'.format(index_str)),
|
problem=format_error_problem(problem, subproblem_prefix='{0}.'.format(index_str)),
|
||||||
@@ -45,58 +48,73 @@ class ModuleFailException(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class ACMEProtocolException(ModuleFailException):
|
class ACMEProtocolException(ModuleFailException):
|
||||||
def __init__(self, module, msg=None, info=None, response=None, content=None, content_json=None):
|
def __init__(self, module, msg=None, info=None, response=None, content=None, content_json=None, extras=None):
|
||||||
# Try to get hold of content, if response is given and content is not provided
|
# 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:
|
if content is None and content_json is None and response is not None:
|
||||||
try:
|
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()
|
content = response.read()
|
||||||
except AttributeError:
|
except (AttributeError, TypeError):
|
||||||
content = info.pop('body', None)
|
content = info.pop('body', None)
|
||||||
|
|
||||||
|
# Make sure that content_json is None or a dictionary
|
||||||
|
if content_json is not None and not isinstance(content_json, dict):
|
||||||
|
if content is None and isinstance(content_json, binary_type):
|
||||||
|
content = content_json
|
||||||
|
content_json = None
|
||||||
|
|
||||||
# Try to get hold of JSON decoded content, when content is given and JSON not provided
|
# Try to get hold of JSON decoded content, when content is given and JSON not provided
|
||||||
if content_json is None and content is not None:
|
if content_json is None and content is not None and module is not None:
|
||||||
try:
|
try:
|
||||||
content_json = module.from_json(content.decode('utf8'))
|
content_json = module.from_json(to_text(content))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
extras = dict()
|
extras = extras or dict()
|
||||||
url = info['url'] if info else None
|
|
||||||
code = info['status'] if info else None
|
|
||||||
extras['http_url'] = url
|
|
||||||
extras['http_status'] = code
|
|
||||||
|
|
||||||
if msg is None:
|
if msg is None:
|
||||||
msg = 'ACME request failed'
|
msg = 'ACME request failed'
|
||||||
add_msg = ''
|
add_msg = ''
|
||||||
|
|
||||||
if code >= 400 and content_json is not None and 'type' in content_json:
|
if info is not None:
|
||||||
if 'status' in content_json and content_json['status'] != code:
|
url = info['url']
|
||||||
code = 'status {problem_code} (HTTP status: {http_code})'.format(http_code=code, problem_code=content_json['status'])
|
code = info['status']
|
||||||
|
extras['http_url'] = url
|
||||||
|
extras['http_status'] = code
|
||||||
|
if code is not None and code >= 400 and content_json is not None and 'type' in content_json:
|
||||||
|
if 'status' in content_json and content_json['status'] != code:
|
||||||
|
code = 'status {problem_code} (HTTP status: {http_code})'.format(http_code=code, problem_code=content_json['status'])
|
||||||
|
else:
|
||||||
|
code = 'status {problem_code}'.format(problem_code=code)
|
||||||
|
subproblems = content_json.pop('subproblems', None)
|
||||||
|
add_msg = ' {problem}.'.format(problem=format_error_problem(content_json))
|
||||||
|
extras['problem'] = content_json
|
||||||
|
extras['subproblems'] = subproblems or []
|
||||||
|
if subproblems is not None:
|
||||||
|
add_msg = '{add_msg} Subproblems:'.format(add_msg=add_msg)
|
||||||
|
for index, problem in enumerate(subproblems):
|
||||||
|
add_msg = '{add_msg}\n({index}) {problem}.'.format(
|
||||||
|
add_msg=add_msg,
|
||||||
|
index=index,
|
||||||
|
problem=format_error_problem(problem, subproblem_prefix='{0}.'.format(index)),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
code = 'status {problem_code}'.format(problem_code=code)
|
code = 'HTTP status {code}'.format(code=code)
|
||||||
add_msg = ' {problem}.'.format(problem=format_error_problem(content_json))
|
if content_json is not None:
|
||||||
|
add_msg = ' The JSON error result: {content}'.format(content=content_json)
|
||||||
subproblems = content_json.pop('subproblems', None)
|
elif content is not None:
|
||||||
extras['problem'] = content_json
|
add_msg = ' The raw error result: {content}'.format(content=to_text(content))
|
||||||
extras['subproblems'] = subproblems or []
|
msg = '{msg} for {url} with {code}'.format(msg=msg, url=url, code=code)
|
||||||
if subproblems is not None:
|
elif content_json is not None:
|
||||||
add_msg = '{add_msg} Subproblems:'.format(add_msg=add_msg)
|
add_msg = ' The JSON result: {content}'.format(content=content_json)
|
||||||
for index, problem in enumerate(subproblems):
|
elif content is not None:
|
||||||
add_msg = '{add_msg}\n({index}) {problem}.'.format(
|
add_msg = ' The raw result: {content}'.format(content=to_text(content))
|
||||||
add_msg=add_msg,
|
|
||||||
index=index,
|
|
||||||
problem=format_error_problem(problem, subproblem_prefix='{0}.'.format(index)),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
code = 'HTTP status {code}'.format(code=code)
|
|
||||||
if content_json is not None:
|
|
||||||
add_msg = ' The JSON error result: {content}'.format(content=content_json)
|
|
||||||
elif content is not None:
|
|
||||||
add_msg = ' The raw error result: {content}'.format(content=content.decode('utf-8'))
|
|
||||||
|
|
||||||
super(ACMEProtocolException, self).__init__(
|
super(ACMEProtocolException, self).__init__(
|
||||||
'{msg} for {url} with {code}.{add_msg}'.format(msg=msg, url=url, code=code, add_msg=add_msg),
|
'{msg}.{add_msg}'.format(msg=msg, add_msg=add_msg),
|
||||||
**extras
|
**extras
|
||||||
)
|
)
|
||||||
self.problem = {}
|
self.problem = {}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
import traceback
|
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
|
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ class Order(object):
|
|||||||
|
|
||||||
if self.status != 'valid':
|
if self.status != 'valid':
|
||||||
raise ACMEProtocolException(
|
raise ACMEProtocolException(
|
||||||
'Failed to wait for order to complete; got status "{status}"'.format(status=self.status), content_json=self.data)
|
client.module,
|
||||||
|
'Failed to wait for order to complete; got status "{status}"'.format(status=self.status),
|
||||||
|
content_json=self.data)
|
||||||
|
|
||||||
def finalize(self, client, csr_der, wait=True):
|
def finalize(self, client, csr_der, wait=True):
|
||||||
'''
|
'''
|
||||||
@@ -121,5 +123,7 @@ class Order(object):
|
|||||||
self.refresh(client)
|
self.refresh(client)
|
||||||
if self.status not in ['procesing', 'valid', 'invalid']:
|
if self.status not in ['procesing', 'valid', 'invalid']:
|
||||||
raise ACMEProtocolException(
|
raise ACMEProtocolException(
|
||||||
'Failed to finalize order; got status "{status}"'.format(
|
client.module,
|
||||||
status=self.status), info=info, content_json=result)
|
'Failed to finalize order; got status "{status}"'.format(status=self.status),
|
||||||
|
info=info,
|
||||||
|
content_json=result)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import re
|
|||||||
import textwrap
|
import textwrap
|
||||||
import traceback
|
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.module_utils.six.moves.urllib.parse import unquote
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
|
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ __metaclass__ = type
|
|||||||
|
|
||||||
import re
|
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
|
__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):
|
def obj2txt(openssl_lib, openssl_ffi, obj):
|
||||||
# Set to 80 on the recommendation of
|
# Set to 80 on the recommendation of
|
||||||
# https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values
|
# https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values
|
||||||
|
|||||||
@@ -23,18 +23,39 @@ import base64
|
|||||||
import binascii
|
import binascii
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text
|
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
|
from ._asn1 import serialize_asn1_string_as_der
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cryptography
|
import cryptography
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
import ipaddress
|
import ipaddress
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Error handled in the calling module.
|
# Error handled in the calling module.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# This is a separate try/except since this is only present in cryptography 2.5 or newer
|
||||||
|
from cryptography.hazmat.primitives.serialization.pkcs12 import (
|
||||||
|
load_key_and_certificates as _load_key_and_certificates,
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
# Error handled in the calling module.
|
||||||
|
_load_key_and_certificates = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# This is a separate try/except since this is only present in cryptography 36.0.0 or newer
|
||||||
|
from cryptography.hazmat.primitives.serialization.pkcs12 import (
|
||||||
|
load_pkcs12 as _load_pkcs12,
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
# Error handled in the calling module.
|
||||||
|
_load_pkcs12 = None
|
||||||
|
|
||||||
from .basic import (
|
from .basic import (
|
||||||
CRYPTOGRAPHY_HAS_ED25519,
|
CRYPTOGRAPHY_HAS_ED25519,
|
||||||
CRYPTOGRAPHY_HAS_ED448,
|
CRYPTOGRAPHY_HAS_ED448,
|
||||||
@@ -55,60 +76,114 @@ DOTTED_OID = re.compile(r'^\d+(?:\.\d+)+$')
|
|||||||
|
|
||||||
|
|
||||||
def cryptography_get_extensions_from_cert(cert):
|
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()
|
result = dict()
|
||||||
backend = cert._backend
|
try:
|
||||||
x509_obj = cert._x509
|
# 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
|
return result
|
||||||
|
|
||||||
|
|
||||||
def cryptography_get_extensions_from_csr(csr):
|
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()
|
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._lib.X509_REQ_get_extensions(csr._x509_req)
|
||||||
extensions = backend._ffi.gc(
|
extensions = backend._ffi.gc(
|
||||||
extensions,
|
extensions,
|
||||||
lambda ext: backend._lib.sk_X509_EXTENSION_pop_free(
|
lambda ext: backend._lib.sk_X509_EXTENSION_pop_free(
|
||||||
ext,
|
ext,
|
||||||
backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free")
|
backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)):
|
# With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does
|
||||||
ext = backend._lib.sk_X509_EXTENSION_value(extensions, i)
|
# not allow to get the raw value of an extension, so we have to use this ugly hack:
|
||||||
if ext == backend._ffi.NULL:
|
exts = list(csr.extensions)
|
||||||
continue
|
|
||||||
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)):
|
||||||
data = backend._lib.X509_EXTENSION_get_data(ext)
|
ext = backend._lib.sk_X509_EXTENSION_value(extensions, i)
|
||||||
backend.openssl_assert(data != backend._ffi.NULL)
|
if ext == backend._ffi.NULL:
|
||||||
der = backend._ffi.buffer(data.data, data.length)[:]
|
continue
|
||||||
entry = dict(
|
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
||||||
critical=(crit == 1),
|
data = backend._lib.X509_EXTENSION_get_data(ext)
|
||||||
value=base64.b64encode(der),
|
backend.openssl_assert(data != backend._ffi.NULL)
|
||||||
)
|
der = backend._ffi.buffer(data.data, data.length)[:]
|
||||||
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
|
entry = dict(
|
||||||
result[oid] = entry
|
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
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -271,11 +346,11 @@ def _dn_escape_value(value):
|
|||||||
'''
|
'''
|
||||||
Escape Distinguished Name's attribute value.
|
Escape Distinguished Name's attribute value.
|
||||||
'''
|
'''
|
||||||
value = value.replace('\\', '\\\\')
|
value = value.replace(u'\\', u'\\\\')
|
||||||
for ch in [',', '#', '+', '<', '>', ';', '"', '=', '/']:
|
for ch in [u',', u'#', u'+', u'<', u'>', u';', u'"', u'=', u'/']:
|
||||||
value = value.replace(ch, '\\%s' % ch)
|
value = value.replace(ch, u'\\%s' % ch)
|
||||||
if value.startswith(' '):
|
if value.startswith(u' '):
|
||||||
value = r'\ ' + value[1:]
|
value = u'\\ ' + value[1:]
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
@@ -285,24 +360,24 @@ def cryptography_decode_name(name):
|
|||||||
Raises an OpenSSLObjectError if the name is not supported.
|
Raises an OpenSSLObjectError if the name is not supported.
|
||||||
'''
|
'''
|
||||||
if isinstance(name, x509.DNSName):
|
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, x509.IPAddress):
|
||||||
if isinstance(name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network)):
|
if isinstance(name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network)):
|
||||||
return 'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen)
|
return u'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen)
|
||||||
return 'IP:{0}'.format(name.value.compressed)
|
return u'IP:{0}'.format(name.value.compressed)
|
||||||
if isinstance(name, x509.RFC822Name):
|
if isinstance(name, x509.RFC822Name):
|
||||||
return 'email:{0}'.format(name.value)
|
return u'email:{0}'.format(name.value)
|
||||||
if isinstance(name, x509.UniformResourceIdentifier):
|
if isinstance(name, x509.UniformResourceIdentifier):
|
||||||
return 'URI:{0}'.format(name.value)
|
return u'URI:{0}'.format(name.value)
|
||||||
if isinstance(name, x509.DirectoryName):
|
if isinstance(name, x509.DirectoryName):
|
||||||
return 'dirName:' + ''.join([
|
return u'dirName:' + u''.join([
|
||||||
'/{0}={1}'.format(cryptography_oid_to_name(attribute.oid, short=True), _dn_escape_value(attribute.value))
|
u'/{0}={1}'.format(to_text(cryptography_oid_to_name(attribute.oid, short=True)), _dn_escape_value(attribute.value))
|
||||||
for attribute in name.value
|
for attribute in name.value
|
||||||
])
|
])
|
||||||
if isinstance(name, x509.RegisteredID):
|
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):
|
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))
|
raise OpenSSLObjectError('Cannot decode name "{0}"'.format(name))
|
||||||
|
|
||||||
|
|
||||||
@@ -428,3 +503,79 @@ def cryptography_serial_number_of_cert(cert):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
# The property was called "serial" before cryptography 1.4
|
# The property was called "serial" before cryptography 1.4
|
||||||
return cert.serial
|
return cert.serial
|
||||||
|
|
||||||
|
|
||||||
|
def parse_pkcs12(pkcs12_bytes, passphrase=None):
|
||||||
|
'''Returns a tuple (private_key, certificate, additional_certificates, friendly_name).
|
||||||
|
'''
|
||||||
|
if _load_pkcs12 is None and _load_key_and_certificates is None:
|
||||||
|
raise ValueError('neither load_pkcs12() nor load_key_and_certificates() present in the current cryptography version')
|
||||||
|
|
||||||
|
if passphrase is not None:
|
||||||
|
passphrase = to_bytes(passphrase)
|
||||||
|
|
||||||
|
# Main code for cryptography 36.0.0 and forward
|
||||||
|
if _load_pkcs12 is not None:
|
||||||
|
return _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase)
|
||||||
|
|
||||||
|
if LooseVersion(cryptography.__version__) >= LooseVersion('35.0'):
|
||||||
|
return _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase)
|
||||||
|
|
||||||
|
return _parse_pkcs12_legacy(pkcs12_bytes, passphrase)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase=None):
|
||||||
|
# Requires cryptography 36.0.0 or newer
|
||||||
|
pkcs12 = _load_pkcs12(pkcs12_bytes, passphrase)
|
||||||
|
additional_certificates = [cert.certificate for cert in pkcs12.additional_certs]
|
||||||
|
private_key = pkcs12.key
|
||||||
|
certificate = None
|
||||||
|
friendly_name = None
|
||||||
|
if pkcs12.cert:
|
||||||
|
certificate = pkcs12.cert.certificate
|
||||||
|
friendly_name = pkcs12.cert.friendly_name
|
||||||
|
return private_key, certificate, additional_certificates, friendly_name
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase=None):
|
||||||
|
# Backwards compatibility code for cryptography 35.x
|
||||||
|
private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase)
|
||||||
|
|
||||||
|
friendly_name = None
|
||||||
|
if certificate:
|
||||||
|
# See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238
|
||||||
|
backend = default_backend()
|
||||||
|
|
||||||
|
# This code basically does what load_key_and_certificates() does, but without error-checking.
|
||||||
|
# Since load_key_and_certificates succeeded, it should not fail.
|
||||||
|
pkcs12 = backend._ffi.gc(
|
||||||
|
backend._lib.d2i_PKCS12_bio(backend._bytes_to_bio(pkcs12_bytes).bio, backend._ffi.NULL),
|
||||||
|
backend._lib.PKCS12_free)
|
||||||
|
certificate_x509_ptr = backend._ffi.new("X509 **")
|
||||||
|
with backend._zeroed_null_terminated_buf(to_bytes(passphrase) if passphrase is not None else None) as passphrase_buffer:
|
||||||
|
backend._lib.PKCS12_parse(
|
||||||
|
pkcs12,
|
||||||
|
passphrase_buffer,
|
||||||
|
backend._ffi.new("EVP_PKEY **"),
|
||||||
|
certificate_x509_ptr,
|
||||||
|
backend._ffi.new("Cryptography_STACK_OF_X509 **"))
|
||||||
|
if certificate_x509_ptr[0] != backend._ffi.NULL:
|
||||||
|
maybe_name = backend._lib.X509_alias_get0(certificate_x509_ptr[0], backend._ffi.NULL)
|
||||||
|
if maybe_name != backend._ffi.NULL:
|
||||||
|
friendly_name = backend._ffi.string(maybe_name)
|
||||||
|
|
||||||
|
return private_key, certificate, additional_certificates, friendly_name
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_pkcs12_legacy(pkcs12_bytes, passphrase=None):
|
||||||
|
# Backwards compatibility code for cryptography < 35.0.0
|
||||||
|
private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase)
|
||||||
|
|
||||||
|
friendly_name = None
|
||||||
|
if certificate:
|
||||||
|
# See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238
|
||||||
|
backend = certificate._backend
|
||||||
|
maybe_name = backend._lib.X509_alias_get0(certificate._x509, backend._ffi.NULL)
|
||||||
|
if maybe_name != backend._ffi.NULL:
|
||||||
|
friendly_name = backend._ffi.string(maybe_name)
|
||||||
|
return private_key, certificate, additional_certificates, friendly_name
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
|||||||
cryptography_compare_public_keys,
|
cryptography_compare_public_keys,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
|
||||||
|
get_certificate_info,
|
||||||
|
)
|
||||||
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
|
||||||
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||||
|
|
||||||
@@ -95,6 +99,19 @@ class CertificateBackend(object):
|
|||||||
self.check_csr_subject = True
|
self.check_csr_subject = True
|
||||||
self.check_csr_extensions = True
|
self.check_csr_extensions = True
|
||||||
|
|
||||||
|
self.diff_before = self._get_info(None)
|
||||||
|
self.diff_after = self._get_info(None)
|
||||||
|
|
||||||
|
def _get_info(self, data):
|
||||||
|
if data is None:
|
||||||
|
return dict()
|
||||||
|
try:
|
||||||
|
result = get_certificate_info(self.module, self.backend, data, prefer_one_fingerprint=True)
|
||||||
|
result['can_parse_certificate'] = True
|
||||||
|
return result
|
||||||
|
except Exception as exc:
|
||||||
|
return dict(can_parse_certificate=False)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def generate_certificate(self):
|
def generate_certificate(self):
|
||||||
"""(Re-)Generate certificate."""
|
"""(Re-)Generate certificate."""
|
||||||
@@ -108,6 +125,7 @@ class CertificateBackend(object):
|
|||||||
def set_existing(self, certificate_bytes):
|
def set_existing(self, certificate_bytes):
|
||||||
"""Set existing certificate bytes. None indicates that the key does not exist."""
|
"""Set existing certificate bytes. None indicates that the key does not exist."""
|
||||||
self.existing_certificate_bytes = certificate_bytes
|
self.existing_certificate_bytes = certificate_bytes
|
||||||
|
self.diff_after = self.diff_before = self._get_info(self.existing_certificate_bytes)
|
||||||
|
|
||||||
def has_existing(self):
|
def has_existing(self):
|
||||||
"""Query whether an existing certificate is/has been there."""
|
"""Query whether an existing certificate is/has been there."""
|
||||||
@@ -284,13 +302,19 @@ class CertificateBackend(object):
|
|||||||
'privatekey': self.privatekey_path,
|
'privatekey': self.privatekey_path,
|
||||||
'csr': self.csr_path
|
'csr': self.csr_path
|
||||||
}
|
}
|
||||||
|
# Get hold of certificate bytes
|
||||||
|
certificate_bytes = self.existing_certificate_bytes
|
||||||
|
if self.cert is not None:
|
||||||
|
certificate_bytes = self.get_certificate_data()
|
||||||
|
self.diff_after = self._get_info(certificate_bytes)
|
||||||
if include_certificate:
|
if include_certificate:
|
||||||
# Get hold of certificate bytes
|
|
||||||
certificate_bytes = self.existing_certificate_bytes
|
|
||||||
if self.cert is not None:
|
|
||||||
certificate_bytes = self.get_certificate_data()
|
|
||||||
# Store result
|
# Store result
|
||||||
result['certificate'] = certificate_bytes.decode('utf-8') if certificate_bytes else None
|
result['certificate'] = certificate_bytes.decode('utf-8') if certificate_bytes else None
|
||||||
|
|
||||||
|
result['diff'] = dict(
|
||||||
|
before=self.diff_before,
|
||||||
|
after=self.diff_after,
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import traceback
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||||
CertificateError,
|
CertificateError,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ __metaclass__ = type
|
|||||||
import abc
|
import abc
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from ansible.module_utils._text import to_native, to_bytes, to_text
|
from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
parse_name_field,
|
parse_name_field,
|
||||||
@@ -177,25 +177,25 @@ class AssertOnlyCertificateBackend(CertificateBackend):
|
|||||||
if self.privatekey_path is not None or self.privatekey_content is not None:
|
if self.privatekey_path is not None or self.privatekey_content is not None:
|
||||||
if not self._validate_privatekey():
|
if not self._validate_privatekey():
|
||||||
messages.append(
|
messages.append(
|
||||||
'Certificate %s and private key %s do not match' %
|
'Certificate and private key %s do not match' %
|
||||||
(self.path, self.privatekey_path or '(provided in module options)')
|
(self.privatekey_path or '(provided in module options)')
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.csr_path is not None or self.csr_content is not None:
|
if self.csr_path is not None or self.csr_content is not None:
|
||||||
if not self._validate_csr_signature():
|
if not self._validate_csr_signature():
|
||||||
messages.append(
|
messages.append(
|
||||||
'Certificate %s and CSR %s do not match: private key mismatch' %
|
'Certificate and CSR %s do not match: private key mismatch' %
|
||||||
(self.path, self.csr_path or '(provided in module options)')
|
(self.csr_path or '(provided in module options)')
|
||||||
)
|
)
|
||||||
if not self._validate_csr_subject():
|
if not self._validate_csr_subject():
|
||||||
messages.append(
|
messages.append(
|
||||||
'Certificate %s and CSR %s do not match: subject mismatch' %
|
'Certificate and CSR %s do not match: subject mismatch' %
|
||||||
(self.path, self.csr_path or '(provided in module options)')
|
(self.csr_path or '(provided in module options)')
|
||||||
)
|
)
|
||||||
if not self._validate_csr_extensions():
|
if not self._validate_csr_extensions():
|
||||||
messages.append(
|
messages.append(
|
||||||
'Certificate %s and CSR %s do not match: extensions mismatch' %
|
'Certificate and CSR %s do not match: extensions mismatch' %
|
||||||
(self.path, self.csr_path or '(provided in module options)')
|
(self.csr_path or '(provided in module options)')
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.signature_algorithms is not None:
|
if self.signature_algorithms is not None:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import datetime
|
|||||||
import time
|
import time
|
||||||
import os
|
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
|
from ansible_collections.community.crypto.plugins.module_utils.ecs.api import ECSClient, RestOperationException, SessionConfigurationException
|
||||||
|
|
||||||
|
|||||||
565
plugins/module_utils/crypto/module_backends/certificate_info.py
Normal file
565
plugins/module_utils/crypto/module_backends/certificate_info.py
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||||
|
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||||
|
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import binascii
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
|
from ansible.module_utils import six
|
||||||
|
from ansible.module_utils.basic import missing_required_lib
|
||||||
|
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
|
load_certificate,
|
||||||
|
get_fingerprint_of_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||||
|
cryptography_decode_name,
|
||||||
|
cryptography_get_extensions_from_cert,
|
||||||
|
cryptography_oid_to_name,
|
||||||
|
cryptography_serial_number_of_cert,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.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:
|
||||||
|
import cryptography
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||||
|
except ImportError:
|
||||||
|
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||||
|
CRYPTOGRAPHY_FOUND = False
|
||||||
|
else:
|
||||||
|
CRYPTOGRAPHY_FOUND = True
|
||||||
|
|
||||||
|
|
||||||
|
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class CertificateInfoRetrieval(object):
|
||||||
|
def __init__(self, module, backend, content):
|
||||||
|
# content must be a bytes string
|
||||||
|
self.module = module
|
||||||
|
self.backend = backend
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_der_bytes(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_signature_algorithm(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_subject_ordered(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_issuer_ordered(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_version(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_key_usage(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_extended_key_usage(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_basic_constraints(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_ocsp_must_staple(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_subject_alt_name(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_not_before(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_not_after(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_public_key_pem(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_public_key_object(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_subject_key_identifier(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_authority_key_identifier(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_serial_number(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_all_extensions(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_ocsp_uri(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_info(self, prefer_one_fingerprint=False):
|
||||||
|
result = dict()
|
||||||
|
self.cert = load_certificate(None, content=self.content, backend=self.backend)
|
||||||
|
|
||||||
|
result['signature_algorithm'] = self._get_signature_algorithm()
|
||||||
|
subject = self._get_subject_ordered()
|
||||||
|
issuer = self._get_issuer_ordered()
|
||||||
|
result['subject'] = dict()
|
||||||
|
for k, v in subject:
|
||||||
|
result['subject'][k] = v
|
||||||
|
result['subject_ordered'] = subject
|
||||||
|
result['issuer'] = dict()
|
||||||
|
for k, v in issuer:
|
||||||
|
result['issuer'][k] = v
|
||||||
|
result['issuer_ordered'] = issuer
|
||||||
|
result['version'] = self._get_version()
|
||||||
|
result['key_usage'], result['key_usage_critical'] = self._get_key_usage()
|
||||||
|
result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage()
|
||||||
|
result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints()
|
||||||
|
result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple()
|
||||||
|
result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name()
|
||||||
|
|
||||||
|
not_before = self.get_not_before()
|
||||||
|
not_after = self.get_not_after()
|
||||||
|
result['not_before'] = not_before.strftime(TIMESTAMP_FORMAT)
|
||||||
|
result['not_after'] = not_after.strftime(TIMESTAMP_FORMAT)
|
||||||
|
result['expired'] = not_after < datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
result['public_key'] = self._get_public_key_pem()
|
||||||
|
|
||||||
|
public_key_info = get_publickey_info(
|
||||||
|
self.module,
|
||||||
|
self.backend,
|
||||||
|
key=self._get_public_key_object(),
|
||||||
|
prefer_one_fingerprint=prefer_one_fingerprint)
|
||||||
|
result.update({
|
||||||
|
'public_key_type': public_key_info['type'],
|
||||||
|
'public_key_data': public_key_info['public_data'],
|
||||||
|
'public_key_fingerprints': public_key_info['fingerprints'],
|
||||||
|
})
|
||||||
|
|
||||||
|
result['fingerprints'] = get_fingerprint_of_bytes(
|
||||||
|
self._get_der_bytes(), prefer_one=prefer_one_fingerprint)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
aki, aci, acsn = self._get_authority_key_identifier()
|
||||||
|
if aki is not None:
|
||||||
|
aki = to_native(binascii.hexlify(aki))
|
||||||
|
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
||||||
|
result['authority_key_identifier'] = aki
|
||||||
|
result['authority_cert_issuer'] = aci
|
||||||
|
result['authority_cert_serial_number'] = acsn
|
||||||
|
|
||||||
|
result['serial_number'] = self._get_serial_number()
|
||||||
|
result['extensions_by_oid'] = self._get_all_extensions()
|
||||||
|
result['ocsp_uri'] = self._get_ocsp_uri()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
|
||||||
|
"""Validate the supplied cert, using the cryptography backend"""
|
||||||
|
def __init__(self, module, content):
|
||||||
|
super(CertificateInfoRetrievalCryptography, self).__init__(module, 'cryptography', content)
|
||||||
|
|
||||||
|
def _get_der_bytes(self):
|
||||||
|
return self.cert.public_bytes(serialization.Encoding.DER)
|
||||||
|
|
||||||
|
def _get_signature_algorithm(self):
|
||||||
|
return cryptography_oid_to_name(self.cert.signature_algorithm_oid)
|
||||||
|
|
||||||
|
def _get_subject_ordered(self):
|
||||||
|
result = []
|
||||||
|
for attribute in self.cert.subject:
|
||||||
|
result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _get_issuer_ordered(self):
|
||||||
|
result = []
|
||||||
|
for attribute in self.cert.issuer:
|
||||||
|
result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _get_version(self):
|
||||||
|
if self.cert.version == x509.Version.v1:
|
||||||
|
return 1
|
||||||
|
if self.cert.version == x509.Version.v3:
|
||||||
|
return 3
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
def _get_key_usage(self):
|
||||||
|
try:
|
||||||
|
current_key_ext = self.cert.extensions.get_extension_for_class(x509.KeyUsage)
|
||||||
|
current_key_usage = current_key_ext.value
|
||||||
|
key_usage = dict(
|
||||||
|
digital_signature=current_key_usage.digital_signature,
|
||||||
|
content_commitment=current_key_usage.content_commitment,
|
||||||
|
key_encipherment=current_key_usage.key_encipherment,
|
||||||
|
data_encipherment=current_key_usage.data_encipherment,
|
||||||
|
key_agreement=current_key_usage.key_agreement,
|
||||||
|
key_cert_sign=current_key_usage.key_cert_sign,
|
||||||
|
crl_sign=current_key_usage.crl_sign,
|
||||||
|
encipher_only=False,
|
||||||
|
decipher_only=False,
|
||||||
|
)
|
||||||
|
if key_usage['key_agreement']:
|
||||||
|
key_usage.update(dict(
|
||||||
|
encipher_only=current_key_usage.encipher_only,
|
||||||
|
decipher_only=current_key_usage.decipher_only
|
||||||
|
))
|
||||||
|
|
||||||
|
key_usage_names = dict(
|
||||||
|
digital_signature='Digital Signature',
|
||||||
|
content_commitment='Non Repudiation',
|
||||||
|
key_encipherment='Key Encipherment',
|
||||||
|
data_encipherment='Data Encipherment',
|
||||||
|
key_agreement='Key Agreement',
|
||||||
|
key_cert_sign='Certificate Sign',
|
||||||
|
crl_sign='CRL Sign',
|
||||||
|
encipher_only='Encipher Only',
|
||||||
|
decipher_only='Decipher Only',
|
||||||
|
)
|
||||||
|
return sorted([
|
||||||
|
key_usage_names[name] for name, value in key_usage.items() if value
|
||||||
|
]), current_key_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def _get_extended_key_usage(self):
|
||||||
|
try:
|
||||||
|
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
|
||||||
|
return sorted([
|
||||||
|
cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value
|
||||||
|
]), ext_keyusage_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def _get_basic_constraints(self):
|
||||||
|
try:
|
||||||
|
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.BasicConstraints)
|
||||||
|
result = []
|
||||||
|
result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE'))
|
||||||
|
if ext_keyusage_ext.value.path_length is not None:
|
||||||
|
result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length))
|
||||||
|
return sorted(result), ext_keyusage_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def _get_ocsp_must_staple(self):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
# This only works with cryptography >= 2.1
|
||||||
|
tlsfeature_ext = self.cert.extensions.get_extension_for_class(x509.TLSFeature)
|
||||||
|
value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
|
||||||
|
except AttributeError:
|
||||||
|
# Fallback for cryptography < 2.1
|
||||||
|
oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
|
||||||
|
tlsfeature_ext = self.cert.extensions.get_extension_for_oid(oid)
|
||||||
|
value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05"
|
||||||
|
return value, tlsfeature_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def _get_subject_alt_name(self):
|
||||||
|
try:
|
||||||
|
san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||||
|
result = [cryptography_decode_name(san) for san in san_ext.value]
|
||||||
|
return result, san_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def get_not_before(self):
|
||||||
|
return self.cert.not_valid_before
|
||||||
|
|
||||||
|
def get_not_after(self):
|
||||||
|
return self.cert.not_valid_after
|
||||||
|
|
||||||
|
def _get_public_key_pem(self):
|
||||||
|
return self.cert.public_key().public_bytes(
|
||||||
|
serialization.Encoding.PEM,
|
||||||
|
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_public_key_object(self):
|
||||||
|
return self.cert.public_key()
|
||||||
|
|
||||||
|
def _get_subject_key_identifier(self):
|
||||||
|
try:
|
||||||
|
ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
||||||
|
return ext.value.digest
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_authority_key_identifier(self):
|
||||||
|
try:
|
||||||
|
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
||||||
|
issuer = None
|
||||||
|
if ext.value.authority_cert_issuer is not None:
|
||||||
|
issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
|
||||||
|
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
def _get_serial_number(self):
|
||||||
|
return cryptography_serial_number_of_cert(self.cert)
|
||||||
|
|
||||||
|
def _get_all_extensions(self):
|
||||||
|
return cryptography_get_extensions_from_cert(self.cert)
|
||||||
|
|
||||||
|
def _get_ocsp_uri(self):
|
||||||
|
try:
|
||||||
|
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess)
|
||||||
|
for desc in ext.value:
|
||||||
|
if desc.access_method == x509.oid.AuthorityInformationAccessOID.OCSP:
|
||||||
|
if isinstance(desc.access_location, x509.UniformResourceIdentifier):
|
||||||
|
return desc.access_location.value
|
||||||
|
except x509.ExtensionNotFound as dummy:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
if can_use_cryptography:
|
||||||
|
backend = 'cryptography'
|
||||||
|
elif can_use_pyopenssl:
|
||||||
|
backend = 'pyopenssl'
|
||||||
|
|
||||||
|
# Success?
|
||||||
|
if backend == 'auto':
|
||||||
|
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||||
|
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||||
|
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||||
|
MINIMAL_PYOPENSSL_VERSION))
|
||||||
|
|
||||||
|
if backend == 'pyopenssl':
|
||||||
|
if not PYOPENSSL_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||||
|
exception=PYOPENSSL_IMP_ERR)
|
||||||
|
try:
|
||||||
|
getattr(crypto.X509Req, 'get_extensions')
|
||||||
|
except AttributeError:
|
||||||
|
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
|
||||||
|
|
||||||
|
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||||
|
version='2.0.0', collection_name='community.crypto')
|
||||||
|
return backend, CertificateInfoRetrievalPyOpenSSL(module, content)
|
||||||
|
elif backend == 'cryptography':
|
||||||
|
if not CRYPTOGRAPHY_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||||
|
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||||
|
return backend, CertificateInfoRetrievalCryptography(module, content)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||||
@@ -13,7 +13,7 @@ import os
|
|||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
from random import randrange
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
OpenSSLBadPassphraseError,
|
OpenSSLBadPassphraseError,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import os
|
|||||||
|
|
||||||
from random import randrange
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
get_relative_time_option,
|
get_relative_time_option,
|
||||||
|
|||||||
100
plugins/module_utils/crypto/module_backends/crl_info.py
Normal file
100
plugins/module_utils/crypto/module_backends/crl_info.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import missing_required_lib
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||||
|
cryptography_oid_to_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import (
|
||||||
|
TIMESTAMP_FORMAT,
|
||||||
|
cryptography_decode_revoked_certificate,
|
||||||
|
cryptography_dump_revoked,
|
||||||
|
cryptography_get_signature_algorithm_oid_from_crl,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||||
|
identify_pem_format,
|
||||||
|
)
|
||||||
|
|
||||||
|
# crypto_utils
|
||||||
|
|
||||||
|
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
|
||||||
|
|
||||||
|
CRYPTOGRAPHY_IMP_ERR = None
|
||||||
|
try:
|
||||||
|
import cryptography
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||||
|
except ImportError:
|
||||||
|
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||||
|
CRYPTOGRAPHY_FOUND = False
|
||||||
|
else:
|
||||||
|
CRYPTOGRAPHY_FOUND = True
|
||||||
|
|
||||||
|
|
||||||
|
class CRLInfoRetrieval(object):
|
||||||
|
def __init__(self, module, content, list_revoked_certificates=True):
|
||||||
|
# content must be a bytes string
|
||||||
|
self.module = module
|
||||||
|
self.content = content
|
||||||
|
self.list_revoked_certificates = list_revoked_certificates
|
||||||
|
|
||||||
|
def get_info(self):
|
||||||
|
self.crl_pem = identify_pem_format(self.content)
|
||||||
|
try:
|
||||||
|
if self.crl_pem:
|
||||||
|
self.crl = x509.load_pem_x509_crl(self.content, default_backend())
|
||||||
|
else:
|
||||||
|
self.crl = x509.load_der_x509_crl(self.content, default_backend())
|
||||||
|
except ValueError as e:
|
||||||
|
self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e))
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'changed': False,
|
||||||
|
'format': 'pem' if self.crl_pem else 'der',
|
||||||
|
'last_update': None,
|
||||||
|
'next_update': None,
|
||||||
|
'digest': None,
|
||||||
|
'issuer_ordered': None,
|
||||||
|
'issuer': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
|
||||||
|
result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
|
||||||
|
result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl))
|
||||||
|
issuer = []
|
||||||
|
for attribute in self.crl.issuer:
|
||||||
|
issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
||||||
|
result['issuer_ordered'] = issuer
|
||||||
|
result['issuer'] = {}
|
||||||
|
for k, v in issuer:
|
||||||
|
result['issuer'][k] = v
|
||||||
|
if self.list_revoked_certificates:
|
||||||
|
result['revoked_certificates'] = []
|
||||||
|
for cert in self.crl:
|
||||||
|
entry = cryptography_decode_revoked_certificate(cert)
|
||||||
|
result['revoked_certificates'].append(cryptography_dump_revoked(entry))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_crl_info(module, content, list_revoked_certificates=True):
|
||||||
|
if not CRYPTOGRAPHY_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||||
|
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||||
|
|
||||||
|
info = CRLInfoRetrieval(module, content, list_revoked_certificates=list_revoked_certificates)
|
||||||
|
return info.get_info()
|
||||||
@@ -16,7 +16,7 @@ from distutils.version import LooseVersion
|
|||||||
|
|
||||||
from ansible.module_utils import six
|
from ansible.module_utils import six
|
||||||
from ansible.module_utils.basic import missing_required_lib
|
from ansible.module_utils.basic import missing_required_lib
|
||||||
from ansible.module_utils._text import to_bytes, to_text
|
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
OpenSSLObjectError,
|
OpenSSLObjectError,
|
||||||
@@ -48,6 +48,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_
|
|||||||
pyopenssl_parse_name_constraints,
|
pyopenssl_parse_name_constraints,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import (
|
||||||
|
get_csr_info,
|
||||||
|
)
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
|
||||||
|
|
||||||
|
|
||||||
@@ -177,6 +181,20 @@ class CertificateSigningRequestBackend(object):
|
|||||||
self.existing_csr = None
|
self.existing_csr = None
|
||||||
self.existing_csr_bytes = None
|
self.existing_csr_bytes = None
|
||||||
|
|
||||||
|
self.diff_before = self._get_info(None)
|
||||||
|
self.diff_after = self._get_info(None)
|
||||||
|
|
||||||
|
def _get_info(self, data):
|
||||||
|
if data is None:
|
||||||
|
return dict()
|
||||||
|
try:
|
||||||
|
result = get_csr_info(
|
||||||
|
self.module, self.backend, data, validate_signature=False, prefer_one_fingerprint=True)
|
||||||
|
result['can_parse_csr'] = True
|
||||||
|
return result
|
||||||
|
except Exception as exc:
|
||||||
|
return dict(can_parse_csr=False)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def generate_csr(self):
|
def generate_csr(self):
|
||||||
"""(Re-)Generate CSR."""
|
"""(Re-)Generate CSR."""
|
||||||
@@ -190,6 +208,7 @@ class CertificateSigningRequestBackend(object):
|
|||||||
def set_existing(self, csr_bytes):
|
def set_existing(self, csr_bytes):
|
||||||
"""Set existing CSR bytes. None indicates that the CSR does not exist."""
|
"""Set existing CSR bytes. None indicates that the CSR does not exist."""
|
||||||
self.existing_csr_bytes = csr_bytes
|
self.existing_csr_bytes = csr_bytes
|
||||||
|
self.diff_after = self.diff_before = self._get_info(self.existing_csr_bytes)
|
||||||
|
|
||||||
def has_existing(self):
|
def has_existing(self):
|
||||||
"""Query whether an existing CSR is/has been there."""
|
"""Query whether an existing CSR is/has been there."""
|
||||||
@@ -238,13 +257,19 @@ class CertificateSigningRequestBackend(object):
|
|||||||
'name_constraints_permitted': self.name_constraints_permitted,
|
'name_constraints_permitted': self.name_constraints_permitted,
|
||||||
'name_constraints_excluded': self.name_constraints_excluded,
|
'name_constraints_excluded': self.name_constraints_excluded,
|
||||||
}
|
}
|
||||||
|
# Get hold of CSR bytes
|
||||||
|
csr_bytes = self.existing_csr_bytes
|
||||||
|
if self.csr is not None:
|
||||||
|
csr_bytes = self.get_csr_data()
|
||||||
|
self.diff_after = self._get_info(csr_bytes)
|
||||||
if include_csr:
|
if include_csr:
|
||||||
# Get hold of CSR bytes
|
|
||||||
csr_bytes = self.existing_csr_bytes
|
|
||||||
if self.csr is not None:
|
|
||||||
csr_bytes = self.get_csr_data()
|
|
||||||
# Store result
|
# Store result
|
||||||
result['csr'] = csr_bytes.decode('utf-8') if csr_bytes else None
|
result['csr'] = csr_bytes.decode('utf-8') if csr_bytes else None
|
||||||
|
|
||||||
|
result['diff'] = dict(
|
||||||
|
before=self.diff_before,
|
||||||
|
after=self.diff_after,
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -567,7 +592,7 @@ class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBack
|
|||||||
def _check_csr(self):
|
def _check_csr(self):
|
||||||
"""Check whether provided parameters, assuming self.existing_csr and self.privatekey have been populated."""
|
"""Check whether provided parameters, assuming self.existing_csr and self.privatekey have been populated."""
|
||||||
def _check_subject(csr):
|
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]
|
current_subject = [(sub.oid, sub.value) for sub in csr.subject]
|
||||||
return set(subject) == set(current_subject)
|
return set(subject) == set(current_subject)
|
||||||
|
|
||||||
@@ -579,8 +604,8 @@ class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBack
|
|||||||
|
|
||||||
def _check_subjectAltName(extensions):
|
def _check_subjectAltName(extensions):
|
||||||
current_altnames_ext = _find_extension(extensions, cryptography.x509.SubjectAlternativeName)
|
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 []
|
current_altnames = [to_text(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 []
|
altnames = [to_text(cryptography_get_name(altname)) for altname in self.subjectAltName] if self.subjectAltName else []
|
||||||
if set(altnames) != set(current_altnames):
|
if set(altnames) != set(current_altnames):
|
||||||
return False
|
return False
|
||||||
if altnames:
|
if altnames:
|
||||||
@@ -653,10 +678,10 @@ class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBack
|
|||||||
|
|
||||||
def _check_nameConstraints(extensions):
|
def _check_nameConstraints(extensions):
|
||||||
current_nc_ext = _find_extension(extensions, cryptography.x509.NameConstraints)
|
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_perm = [to_text(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 []
|
current_nc_excl = [to_text(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_perm = [to_text(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]
|
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):
|
if set(nc_perm) != set(current_nc_perm) or set(nc_excl) != set(current_nc_excl):
|
||||||
return False
|
return False
|
||||||
if nc_perm or nc_excl:
|
if nc_perm or nc_excl:
|
||||||
@@ -685,9 +710,9 @@ class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBack
|
|||||||
aci = None
|
aci = None
|
||||||
csr_aci = None
|
csr_aci = None
|
||||||
if self.authority_cert_issuer is not 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:
|
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
|
return (ext.value.key_identifier == self.authority_key_identifier
|
||||||
and csr_aci == aci
|
and csr_aci == aci
|
||||||
and ext.value.authority_cert_serial_number == self.authority_cert_serial_number)
|
and ext.value.authority_cert_serial_number == self.authority_cert_serial_number)
|
||||||
|
|||||||
481
plugins/module_utils/crypto/module_backends/csr_info.py
Normal file
481
plugins/module_utils/crypto/module_backends/csr_info.py
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||||
|
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||||
|
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import binascii
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
|
from ansible.module_utils import six
|
||||||
|
from ansible.module_utils.basic import missing_required_lib
|
||||||
|
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
|
load_certificate_request,
|
||||||
|
get_fingerprint_of_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||||
|
cryptography_decode_name,
|
||||||
|
cryptography_get_extensions_from_csr,
|
||||||
|
cryptography_oid_to_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.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:
|
||||||
|
import cryptography
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||||
|
except ImportError:
|
||||||
|
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||||
|
CRYPTOGRAPHY_FOUND = False
|
||||||
|
else:
|
||||||
|
CRYPTOGRAPHY_FOUND = True
|
||||||
|
|
||||||
|
|
||||||
|
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class CSRInfoRetrieval(object):
|
||||||
|
def __init__(self, module, backend, content, validate_signature):
|
||||||
|
# content must be a bytes string
|
||||||
|
self.module = module
|
||||||
|
self.backend = backend
|
||||||
|
self.content = content
|
||||||
|
self.validate_signature = validate_signature
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_subject_ordered(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_key_usage(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_extended_key_usage(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_basic_constraints(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_ocsp_must_staple(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_subject_alt_name(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_name_constraints(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_public_key_pem(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_public_key_object(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_subject_key_identifier(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_authority_key_identifier(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_all_extensions(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _is_signature_valid(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_info(self, prefer_one_fingerprint=False):
|
||||||
|
result = dict()
|
||||||
|
self.csr = load_certificate_request(None, content=self.content, backend=self.backend)
|
||||||
|
|
||||||
|
subject = self._get_subject_ordered()
|
||||||
|
result['subject'] = dict()
|
||||||
|
for k, v in subject:
|
||||||
|
result['subject'][k] = v
|
||||||
|
result['subject_ordered'] = subject
|
||||||
|
result['key_usage'], result['key_usage_critical'] = self._get_key_usage()
|
||||||
|
result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage()
|
||||||
|
result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints()
|
||||||
|
result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple()
|
||||||
|
result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name()
|
||||||
|
(
|
||||||
|
result['name_constraints_permitted'],
|
||||||
|
result['name_constraints_excluded'],
|
||||||
|
result['name_constraints_critical'],
|
||||||
|
) = self._get_name_constraints()
|
||||||
|
|
||||||
|
result['public_key'] = self._get_public_key_pem()
|
||||||
|
|
||||||
|
public_key_info = get_publickey_info(
|
||||||
|
self.module,
|
||||||
|
self.backend,
|
||||||
|
key=self._get_public_key_object(),
|
||||||
|
prefer_one_fingerprint=prefer_one_fingerprint)
|
||||||
|
result.update({
|
||||||
|
'public_key_type': public_key_info['type'],
|
||||||
|
'public_key_data': public_key_info['public_data'],
|
||||||
|
'public_key_fingerprints': public_key_info['fingerprints'],
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
aki, aci, acsn = self._get_authority_key_identifier()
|
||||||
|
if aki is not None:
|
||||||
|
aki = to_native(binascii.hexlify(aki))
|
||||||
|
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
||||||
|
result['authority_key_identifier'] = aki
|
||||||
|
result['authority_cert_issuer'] = aci
|
||||||
|
result['authority_cert_serial_number'] = acsn
|
||||||
|
|
||||||
|
result['extensions_by_oid'] = self._get_all_extensions()
|
||||||
|
|
||||||
|
result['signature_valid'] = self._is_signature_valid()
|
||||||
|
if self.validate_signature and not result['signature_valid']:
|
||||||
|
self.module.fail_json(
|
||||||
|
msg='CSR signature is invalid!',
|
||||||
|
**result
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
|
||||||
|
"""Validate the supplied CSR, using the cryptography backend"""
|
||||||
|
def __init__(self, module, content, validate_signature):
|
||||||
|
super(CSRInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, validate_signature)
|
||||||
|
|
||||||
|
def _get_subject_ordered(self):
|
||||||
|
result = []
|
||||||
|
for attribute in self.csr.subject:
|
||||||
|
result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _get_key_usage(self):
|
||||||
|
try:
|
||||||
|
current_key_ext = self.csr.extensions.get_extension_for_class(x509.KeyUsage)
|
||||||
|
current_key_usage = current_key_ext.value
|
||||||
|
key_usage = dict(
|
||||||
|
digital_signature=current_key_usage.digital_signature,
|
||||||
|
content_commitment=current_key_usage.content_commitment,
|
||||||
|
key_encipherment=current_key_usage.key_encipherment,
|
||||||
|
data_encipherment=current_key_usage.data_encipherment,
|
||||||
|
key_agreement=current_key_usage.key_agreement,
|
||||||
|
key_cert_sign=current_key_usage.key_cert_sign,
|
||||||
|
crl_sign=current_key_usage.crl_sign,
|
||||||
|
encipher_only=False,
|
||||||
|
decipher_only=False,
|
||||||
|
)
|
||||||
|
if key_usage['key_agreement']:
|
||||||
|
key_usage.update(dict(
|
||||||
|
encipher_only=current_key_usage.encipher_only,
|
||||||
|
decipher_only=current_key_usage.decipher_only
|
||||||
|
))
|
||||||
|
|
||||||
|
key_usage_names = dict(
|
||||||
|
digital_signature='Digital Signature',
|
||||||
|
content_commitment='Non Repudiation',
|
||||||
|
key_encipherment='Key Encipherment',
|
||||||
|
data_encipherment='Data Encipherment',
|
||||||
|
key_agreement='Key Agreement',
|
||||||
|
key_cert_sign='Certificate Sign',
|
||||||
|
crl_sign='CRL Sign',
|
||||||
|
encipher_only='Encipher Only',
|
||||||
|
decipher_only='Decipher Only',
|
||||||
|
)
|
||||||
|
return sorted([
|
||||||
|
key_usage_names[name] for name, value in key_usage.items() if value
|
||||||
|
]), current_key_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def _get_extended_key_usage(self):
|
||||||
|
try:
|
||||||
|
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
|
||||||
|
return sorted([
|
||||||
|
cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value
|
||||||
|
]), ext_keyusage_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def _get_basic_constraints(self):
|
||||||
|
try:
|
||||||
|
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.BasicConstraints)
|
||||||
|
result = ['CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE')]
|
||||||
|
if ext_keyusage_ext.value.path_length is not None:
|
||||||
|
result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length))
|
||||||
|
return sorted(result), ext_keyusage_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def _get_ocsp_must_staple(self):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
# This only works with cryptography >= 2.1
|
||||||
|
tlsfeature_ext = self.csr.extensions.get_extension_for_class(x509.TLSFeature)
|
||||||
|
value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
|
||||||
|
except AttributeError:
|
||||||
|
# Fallback for cryptography < 2.1
|
||||||
|
oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
|
||||||
|
tlsfeature_ext = self.csr.extensions.get_extension_for_oid(oid)
|
||||||
|
value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05"
|
||||||
|
return value, tlsfeature_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def _get_subject_alt_name(self):
|
||||||
|
try:
|
||||||
|
san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||||
|
result = [cryptography_decode_name(san) for san in san_ext.value]
|
||||||
|
return result, san_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def _get_name_constraints(self):
|
||||||
|
try:
|
||||||
|
nc_ext = self.csr.extensions.get_extension_for_class(x509.NameConstraints)
|
||||||
|
permitted = [cryptography_decode_name(san) for san in nc_ext.value.permitted_subtrees or []]
|
||||||
|
excluded = [cryptography_decode_name(san) for san in nc_ext.value.excluded_subtrees or []]
|
||||||
|
return permitted, excluded, nc_ext.critical
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, None, False
|
||||||
|
|
||||||
|
def _get_public_key_pem(self):
|
||||||
|
return self.csr.public_key().public_bytes(
|
||||||
|
serialization.Encoding.PEM,
|
||||||
|
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_public_key_object(self):
|
||||||
|
return self.csr.public_key()
|
||||||
|
|
||||||
|
def _get_subject_key_identifier(self):
|
||||||
|
try:
|
||||||
|
ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
||||||
|
return ext.value.digest
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_authority_key_identifier(self):
|
||||||
|
try:
|
||||||
|
ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
||||||
|
issuer = None
|
||||||
|
if ext.value.authority_cert_issuer is not None:
|
||||||
|
issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
|
||||||
|
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
||||||
|
except cryptography.x509.ExtensionNotFound:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
def _get_all_extensions(self):
|
||||||
|
return cryptography_get_extensions_from_csr(self.csr)
|
||||||
|
|
||||||
|
def _is_signature_valid(self):
|
||||||
|
return self.csr.is_signature_valid
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
if can_use_cryptography:
|
||||||
|
backend = 'cryptography'
|
||||||
|
elif can_use_pyopenssl:
|
||||||
|
backend = 'pyopenssl'
|
||||||
|
|
||||||
|
# Success?
|
||||||
|
if backend == 'auto':
|
||||||
|
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||||
|
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||||
|
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||||
|
MINIMAL_PYOPENSSL_VERSION))
|
||||||
|
|
||||||
|
if backend == 'pyopenssl':
|
||||||
|
if not PYOPENSSL_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||||
|
exception=PYOPENSSL_IMP_ERR)
|
||||||
|
try:
|
||||||
|
getattr(crypto.X509Req, 'get_extensions')
|
||||||
|
except AttributeError:
|
||||||
|
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
|
||||||
|
|
||||||
|
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||||
|
version='2.0.0', collection_name='community.crypto')
|
||||||
|
return backend, CSRInfoRetrievalPyOpenSSL(module, content, validate_signature=validate_signature)
|
||||||
|
elif backend == 'cryptography':
|
||||||
|
if not CRYPTOGRAPHY_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||||
|
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||||
|
return backend, CSRInfoRetrievalCryptography(module, content, validate_signature=validate_signature)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||||
@@ -16,7 +16,7 @@ from distutils.version import LooseVersion
|
|||||||
|
|
||||||
from ansible.module_utils import six
|
from ansible.module_utils import six
|
||||||
from ansible.module_utils.basic import missing_required_lib
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
CRYPTOGRAPHY_HAS_X25519,
|
CRYPTOGRAPHY_HAS_X25519,
|
||||||
@@ -37,6 +37,12 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import
|
|||||||
identify_private_key_format,
|
identify_private_key_format,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import (
|
||||||
|
PrivateKeyConsistencyError,
|
||||||
|
PrivateKeyParseError,
|
||||||
|
get_privatekey_info,
|
||||||
|
)
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
|
||||||
|
|
||||||
|
|
||||||
@@ -102,6 +108,25 @@ class PrivateKeyBackend:
|
|||||||
self.existing_private_key = None
|
self.existing_private_key = None
|
||||||
self.existing_private_key_bytes = None
|
self.existing_private_key_bytes = None
|
||||||
|
|
||||||
|
self.diff_before = self._get_info(None)
|
||||||
|
self.diff_after = self._get_info(None)
|
||||||
|
|
||||||
|
def _get_info(self, data):
|
||||||
|
if data is None:
|
||||||
|
return dict()
|
||||||
|
result = dict(can_parse_key=False)
|
||||||
|
try:
|
||||||
|
result.update(get_privatekey_info(
|
||||||
|
self.module, self.backend, data, passphrase=self.passphrase,
|
||||||
|
return_private_key_data=False, prefer_one_fingerprint=True))
|
||||||
|
except PrivateKeyConsistencyError as exc:
|
||||||
|
result.update(exc.result)
|
||||||
|
except PrivateKeyParseError as exc:
|
||||||
|
result.update(exc.result)
|
||||||
|
except Exception as exc:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def generate_private_key(self):
|
def generate_private_key(self):
|
||||||
"""(Re-)Generate private key."""
|
"""(Re-)Generate private key."""
|
||||||
@@ -125,6 +150,7 @@ class PrivateKeyBackend:
|
|||||||
def set_existing(self, privatekey_bytes):
|
def set_existing(self, privatekey_bytes):
|
||||||
"""Set existing private key bytes. None indicates that the key does not exist."""
|
"""Set existing private key bytes. None indicates that the key does not exist."""
|
||||||
self.existing_private_key_bytes = privatekey_bytes
|
self.existing_private_key_bytes = privatekey_bytes
|
||||||
|
self.diff_after = self.diff_before = self._get_info(self.existing_private_key_bytes)
|
||||||
|
|
||||||
def has_existing(self):
|
def has_existing(self):
|
||||||
"""Query whether an existing private key is/has been there."""
|
"""Query whether an existing private key is/has been there."""
|
||||||
@@ -215,11 +241,12 @@ class PrivateKeyBackend:
|
|||||||
}
|
}
|
||||||
if self.type == 'ECC':
|
if self.type == 'ECC':
|
||||||
result['curve'] = self.curve
|
result['curve'] = self.curve
|
||||||
|
# Get hold of private key bytes
|
||||||
|
pk_bytes = self.existing_private_key_bytes
|
||||||
|
if self.private_key is not None:
|
||||||
|
pk_bytes = self.get_private_key_data()
|
||||||
|
self.diff_after = self._get_info(pk_bytes)
|
||||||
if include_key:
|
if include_key:
|
||||||
# Get hold of private key bytes
|
|
||||||
pk_bytes = self.existing_private_key_bytes
|
|
||||||
if self.private_key is not None:
|
|
||||||
pk_bytes = self.get_private_key_data()
|
|
||||||
# Store result
|
# Store result
|
||||||
if pk_bytes:
|
if pk_bytes:
|
||||||
if identify_private_key_format(pk_bytes) == 'raw':
|
if identify_private_key_format(pk_bytes) == 'raw':
|
||||||
@@ -229,6 +256,10 @@ class PrivateKeyBackend:
|
|||||||
else:
|
else:
|
||||||
result['privatekey'] = None
|
result['privatekey'] = None
|
||||||
|
|
||||||
|
result['diff'] = dict(
|
||||||
|
before=self.diff_before,
|
||||||
|
after=self.diff_after,
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
434
plugins/module_utils/crypto/module_backends/privatekey_info.py
Normal file
434
plugins/module_utils/crypto/module_backends/privatekey_info.py
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||||
|
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||||
|
# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
|
from ansible.module_utils import six
|
||||||
|
from ansible.module_utils.basic import missing_required_lib
|
||||||
|
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
|
CRYPTOGRAPHY_HAS_ED25519,
|
||||||
|
CRYPTOGRAPHY_HAS_ED448,
|
||||||
|
OpenSSLObjectError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
|
load_privatekey,
|
||||||
|
get_fingerprint_of_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
|
||||||
|
binary_exp_mod,
|
||||||
|
quick_is_not_prime,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||||
|
_get_cryptography_public_key_info,
|
||||||
|
_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:
|
||||||
|
import cryptography
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||||
|
except ImportError:
|
||||||
|
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||||
|
CRYPTOGRAPHY_FOUND = False
|
||||||
|
else:
|
||||||
|
CRYPTOGRAPHY_FOUND = True
|
||||||
|
|
||||||
|
SIGNATURE_TEST_DATA = b'1234'
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cryptography_private_key_info(key):
|
||||||
|
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
|
||||||
|
return key_type, key_public_data, key_private_data
|
||||||
|
|
||||||
|
|
||||||
|
def _check_dsa_consistency(key_public_data, key_private_data):
|
||||||
|
# Get parameters
|
||||||
|
p = key_public_data.get('p')
|
||||||
|
q = key_public_data.get('q')
|
||||||
|
g = key_public_data.get('g')
|
||||||
|
y = key_public_data.get('y')
|
||||||
|
x = key_private_data.get('x')
|
||||||
|
for v in (p, q, g, y, x):
|
||||||
|
if v is None:
|
||||||
|
return None
|
||||||
|
# Make sure that g is not 0, 1 or -1 in Z/pZ
|
||||||
|
if g < 2 or g >= p - 1:
|
||||||
|
return False
|
||||||
|
# Make sure that x is in range
|
||||||
|
if x < 1 or x >= q:
|
||||||
|
return False
|
||||||
|
# Check whether q divides p-1
|
||||||
|
if (p - 1) % q != 0:
|
||||||
|
return False
|
||||||
|
# Check that g**q mod p == 1
|
||||||
|
if binary_exp_mod(g, q, p) != 1:
|
||||||
|
return False
|
||||||
|
# Check whether g**x mod p == y
|
||||||
|
if binary_exp_mod(g, x, p) != y:
|
||||||
|
return False
|
||||||
|
# Check (quickly) whether p or q are not primes
|
||||||
|
if quick_is_not_prime(q) or quick_is_not_prime(p):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _is_cryptography_key_consistent(key, key_public_data, key_private_data):
|
||||||
|
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||||
|
return bool(key._backend._lib.RSA_check_key(key._rsa_cdata))
|
||||||
|
if isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
||||||
|
result = _check_dsa_consistency(key_public_data, key_private_data)
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
try:
|
||||||
|
signature = key.sign(SIGNATURE_TEST_DATA, cryptography.hazmat.primitives.hashes.SHA256())
|
||||||
|
except AttributeError:
|
||||||
|
# sign() was added in cryptography 1.5, but we support older versions
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
key.public_key().verify(
|
||||||
|
signature,
|
||||||
|
SIGNATURE_TEST_DATA,
|
||||||
|
cryptography.hazmat.primitives.hashes.SHA256()
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except cryptography.exceptions.InvalidSignature:
|
||||||
|
return False
|
||||||
|
if isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||||
|
try:
|
||||||
|
signature = key.sign(
|
||||||
|
SIGNATURE_TEST_DATA,
|
||||||
|
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256())
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
# sign() was added in cryptography 1.5, but we support older versions
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
key.public_key().verify(
|
||||||
|
signature,
|
||||||
|
SIGNATURE_TEST_DATA,
|
||||||
|
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256())
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except cryptography.exceptions.InvalidSignature:
|
||||||
|
return False
|
||||||
|
has_simple_sign_function = False
|
||||||
|
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
||||||
|
has_simple_sign_function = True
|
||||||
|
if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
||||||
|
has_simple_sign_function = True
|
||||||
|
if has_simple_sign_function:
|
||||||
|
signature = key.sign(SIGNATURE_TEST_DATA)
|
||||||
|
try:
|
||||||
|
key.public_key().verify(signature, SIGNATURE_TEST_DATA)
|
||||||
|
return True
|
||||||
|
except cryptography.exceptions.InvalidSignature:
|
||||||
|
return False
|
||||||
|
# For X25519 and X448, there's no test yet.
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class PrivateKeyConsistencyError(OpenSSLObjectError):
|
||||||
|
def __init__(self, msg, result):
|
||||||
|
super(PrivateKeyConsistencyError, self).__init__(msg)
|
||||||
|
self.error_message = msg
|
||||||
|
self.result = result
|
||||||
|
|
||||||
|
|
||||||
|
class PrivateKeyParseError(OpenSSLObjectError):
|
||||||
|
def __init__(self, msg, result):
|
||||||
|
super(PrivateKeyParseError, self).__init__(msg)
|
||||||
|
self.error_message = msg
|
||||||
|
self.result = result
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class PrivateKeyInfoRetrieval(object):
|
||||||
|
def __init__(self, module, backend, content, passphrase=None, return_private_key_data=False):
|
||||||
|
# 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
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_public_key(self, binary):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_key_info(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _is_key_consistent(self, key_public_data, key_private_data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_info(self, prefer_one_fingerprint=False):
|
||||||
|
result = dict(
|
||||||
|
can_parse_key=False,
|
||||||
|
key_is_consistent=None,
|
||||||
|
)
|
||||||
|
priv_key_detail = self.content
|
||||||
|
try:
|
||||||
|
self.key = load_privatekey(
|
||||||
|
path=None,
|
||||||
|
content=priv_key_detail,
|
||||||
|
passphrase=to_bytes(self.passphrase) if self.passphrase is not None else self.passphrase,
|
||||||
|
backend=self.backend
|
||||||
|
)
|
||||||
|
result['can_parse_key'] = True
|
||||||
|
except OpenSSLObjectError as exc:
|
||||||
|
raise PrivateKeyParseError(to_native(exc), result)
|
||||||
|
|
||||||
|
result['public_key'] = self._get_public_key(binary=False)
|
||||||
|
pk = self._get_public_key(binary=True)
|
||||||
|
result['public_key_fingerprints'] = get_fingerprint_of_bytes(
|
||||||
|
pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict()
|
||||||
|
|
||||||
|
key_type, key_public_data, key_private_data = self._get_key_info()
|
||||||
|
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)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class PrivateKeyInfoRetrievalCryptography(PrivateKeyInfoRetrieval):
|
||||||
|
"""Validate the supplied private key, using the cryptography backend"""
|
||||||
|
def __init__(self, module, content, **kwargs):
|
||||||
|
super(PrivateKeyInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, **kwargs)
|
||||||
|
|
||||||
|
def _get_public_key(self, binary):
|
||||||
|
return self.key.public_key().public_bytes(
|
||||||
|
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
||||||
|
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_key_info(self):
|
||||||
|
return _get_cryptography_private_key_info(self.key)
|
||||||
|
|
||||||
|
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):
|
||||||
|
if backend == 'auto':
|
||||||
|
# Detection what is possible
|
||||||
|
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||||
|
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||||
|
|
||||||
|
# First try cryptography, then pyOpenSSL
|
||||||
|
if can_use_cryptography:
|
||||||
|
backend = 'cryptography'
|
||||||
|
elif can_use_pyopenssl:
|
||||||
|
backend = 'pyopenssl'
|
||||||
|
|
||||||
|
# Success?
|
||||||
|
if backend == 'auto':
|
||||||
|
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||||
|
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||||
|
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||||
|
MINIMAL_PYOPENSSL_VERSION))
|
||||||
|
|
||||||
|
if backend == 'pyopenssl':
|
||||||
|
if not PYOPENSSL_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||||
|
exception=PYOPENSSL_IMP_ERR)
|
||||||
|
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 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)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||||
320
plugins/module_utils/crypto/module_backends/publickey_info.py
Normal file
320
plugins/module_utils/crypto/module_backends/publickey_info.py
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright: (c) 2020-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 abc
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
|
from ansible.module_utils import six
|
||||||
|
from ansible.module_utils.basic import missing_required_lib
|
||||||
|
from ansible.module_utils.common.text.converters import to_native
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
|
CRYPTOGRAPHY_HAS_X25519,
|
||||||
|
CRYPTOGRAPHY_HAS_X448,
|
||||||
|
CRYPTOGRAPHY_HAS_ED25519,
|
||||||
|
CRYPTOGRAPHY_HAS_ED448,
|
||||||
|
OpenSSLObjectError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
|
get_fingerprint_of_bytes,
|
||||||
|
load_publickey,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||||
|
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:
|
||||||
|
import cryptography
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||||
|
except ImportError:
|
||||||
|
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||||
|
CRYPTOGRAPHY_FOUND = False
|
||||||
|
else:
|
||||||
|
CRYPTOGRAPHY_FOUND = True
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cryptography_public_key_info(key):
|
||||||
|
key_public_data = dict()
|
||||||
|
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
|
||||||
|
key_type = 'RSA'
|
||||||
|
public_numbers = key.public_numbers()
|
||||||
|
key_public_data['size'] = key.key_size
|
||||||
|
key_public_data['modulus'] = public_numbers.n
|
||||||
|
key_public_data['exponent'] = public_numbers.e
|
||||||
|
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey):
|
||||||
|
key_type = 'DSA'
|
||||||
|
parameter_numbers = key.parameters().parameter_numbers()
|
||||||
|
public_numbers = key.public_numbers()
|
||||||
|
key_public_data['size'] = key.key_size
|
||||||
|
key_public_data['p'] = parameter_numbers.p
|
||||||
|
key_public_data['q'] = parameter_numbers.q
|
||||||
|
key_public_data['g'] = parameter_numbers.g
|
||||||
|
key_public_data['y'] = public_numbers.y
|
||||||
|
elif CRYPTOGRAPHY_HAS_X25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey):
|
||||||
|
key_type = 'X25519'
|
||||||
|
elif CRYPTOGRAPHY_HAS_X448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey):
|
||||||
|
key_type = 'X448'
|
||||||
|
elif CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey):
|
||||||
|
key_type = 'Ed25519'
|
||||||
|
elif CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey):
|
||||||
|
key_type = 'Ed448'
|
||||||
|
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
|
||||||
|
key_type = 'ECC'
|
||||||
|
public_numbers = key.public_numbers()
|
||||||
|
key_public_data['curve'] = key.curve.name
|
||||||
|
key_public_data['x'] = public_numbers.x
|
||||||
|
key_public_data['y'] = public_numbers.y
|
||||||
|
key_public_data['exponent_size'] = key.curve.key_size
|
||||||
|
else:
|
||||||
|
key_type = 'unknown ({0})'.format(type(key))
|
||||||
|
return key_type, key_public_data
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.error_message = msg
|
||||||
|
self.result = result
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class PublicKeyInfoRetrieval(object):
|
||||||
|
def __init__(self, module, backend, content=None, key=None):
|
||||||
|
# content must be a bytes string
|
||||||
|
self.module = module
|
||||||
|
self.backend = backend
|
||||||
|
self.content = content
|
||||||
|
self.key = key
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_public_key(self, binary):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_key_info(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_info(self, prefer_one_fingerprint=False):
|
||||||
|
result = dict()
|
||||||
|
if self.key is None:
|
||||||
|
try:
|
||||||
|
self.key = load_publickey(content=self.content, backend=self.backend)
|
||||||
|
except OpenSSLObjectError as e:
|
||||||
|
raise PublicKeyParseError(to_native(e))
|
||||||
|
|
||||||
|
pk = self._get_public_key(binary=True)
|
||||||
|
result['fingerprints'] = get_fingerprint_of_bytes(
|
||||||
|
pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict()
|
||||||
|
|
||||||
|
key_type, key_public_data = self._get_key_info()
|
||||||
|
result['type'] = key_type
|
||||||
|
result['public_data'] = key_public_data
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class PublicKeyInfoRetrievalCryptography(PublicKeyInfoRetrieval):
|
||||||
|
"""Validate the supplied public key, using the cryptography backend"""
|
||||||
|
def __init__(self, module, content=None, key=None):
|
||||||
|
super(PublicKeyInfoRetrievalCryptography, self).__init__(module, 'cryptography', content=content, key=key)
|
||||||
|
|
||||||
|
def _get_public_key(self, binary):
|
||||||
|
return self.key.public_bytes(
|
||||||
|
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
||||||
|
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_key_info(self):
|
||||||
|
return _get_cryptography_public_key_info(self.key)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
if can_use_cryptography:
|
||||||
|
backend = 'cryptography'
|
||||||
|
elif can_use_pyopenssl:
|
||||||
|
backend = 'pyopenssl'
|
||||||
|
|
||||||
|
# Success?
|
||||||
|
if backend == 'auto':
|
||||||
|
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
||||||
|
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
||||||
|
MINIMAL_CRYPTOGRAPHY_VERSION,
|
||||||
|
MINIMAL_PYOPENSSL_VERSION))
|
||||||
|
|
||||||
|
if backend == 'pyopenssl':
|
||||||
|
if not PYOPENSSL_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||||
|
exception=PYOPENSSL_IMP_ERR)
|
||||||
|
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
||||||
|
version='2.0.0', collection_name='community.crypto')
|
||||||
|
return backend, PublicKeyInfoRetrievalPyOpenSSL(module, content=content, key=key)
|
||||||
|
elif backend == 'cryptography':
|
||||||
|
if not CRYPTOGRAPHY_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||||
|
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||||
|
return backend, PublicKeyInfoRetrievalCryptography(module, content=content, key=key)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||||
@@ -18,19 +18,7 @@
|
|||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
|
# This import is only to maintain backwards compatibility
|
||||||
import re
|
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
|
||||||
|
parse_openssh_version
|
||||||
|
)
|
||||||
def parse_openssh_version(version_string):
|
|
||||||
"""Parse the version output of ssh -V and return version numbers that can be compared"""
|
|
||||||
|
|
||||||
parsed_result = re.match(
|
|
||||||
r"^.*openssh_(?P<version>[0-9.]+)(p?[0-9]+)[^0-9]*.*$", version_string.lower()
|
|
||||||
)
|
|
||||||
if parsed_result is not None:
|
|
||||||
version = parsed_result.group("version").strip()
|
|
||||||
else:
|
|
||||||
version = None
|
|
||||||
|
|
||||||
return version
|
|
||||||
|
|||||||
@@ -72,3 +72,13 @@ def split_pem_list(text, keep_inbetween=False):
|
|||||||
result.append(''.join(current))
|
result.append(''.join(current))
|
||||||
current = [] if keep_inbetween else None
|
current = [] if keep_inbetween else None
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def extract_first_pem(text):
|
||||||
|
'''
|
||||||
|
Given one PEM or multiple concatenated PEM objects, return only the first one, or None if there is none.
|
||||||
|
'''
|
||||||
|
all_pems = split_pem_list(text)
|
||||||
|
if not all_pems:
|
||||||
|
return None
|
||||||
|
return all_pems[0]
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ __metaclass__ = type
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from ansible.module_utils._text import to_bytes, to_text
|
from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
||||||
|
|
||||||
|
from ._objects import OID_LOOKUP
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import OpenSSL
|
import OpenSSL
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -87,18 +89,25 @@ def pyopenssl_get_extensions_from_cert(cert):
|
|||||||
critical=bool(ext.get_critical()),
|
critical=bool(ext.get_critical()),
|
||||||
value=base64.b64encode(ext.get_data()),
|
value=base64.b64encode(ext.get_data()),
|
||||||
)
|
)
|
||||||
oid = obj2txt(
|
try:
|
||||||
OpenSSL._util.lib,
|
oid = obj2txt(
|
||||||
OpenSSL._util.ffi,
|
OpenSSL._util.lib,
|
||||||
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
OpenSSL._util.ffi,
|
||||||
)
|
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
||||||
# This could also be done a bit simpler:
|
)
|
||||||
#
|
# This could also be done a bit simpler:
|
||||||
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
|
#
|
||||||
#
|
# 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
|
# Unfortunately this gives the wrong result in case the linked OpenSSL
|
||||||
# similarly to how cryptography does it.
|
# doesn't know the OID. That's why we have to get the OID dotted string
|
||||||
|
# similarly to how cryptography does it.
|
||||||
|
except AttributeError:
|
||||||
|
# When PyOpenSSL is used with cryptography >= 35.0.0, obj2txt cannot be used.
|
||||||
|
# We try to figure out the OID with our internal lookup table, and if we fail,
|
||||||
|
# we use the short name OpenSSL returns.
|
||||||
|
oid = to_native(ext.get_short_name())
|
||||||
|
oid = OID_LOOKUP.get(oid, oid)
|
||||||
result[oid] = entry
|
result[oid] = entry
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -113,18 +122,25 @@ def pyopenssl_get_extensions_from_csr(csr):
|
|||||||
critical=bool(ext.get_critical()),
|
critical=bool(ext.get_critical()),
|
||||||
value=base64.b64encode(ext.get_data()),
|
value=base64.b64encode(ext.get_data()),
|
||||||
)
|
)
|
||||||
oid = obj2txt(
|
try:
|
||||||
OpenSSL._util.lib,
|
oid = obj2txt(
|
||||||
OpenSSL._util.ffi,
|
OpenSSL._util.lib,
|
||||||
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
OpenSSL._util.ffi,
|
||||||
)
|
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
||||||
# This could also be done a bit simpler:
|
)
|
||||||
#
|
# This could also be done a bit simpler:
|
||||||
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
|
#
|
||||||
#
|
# 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
|
# Unfortunately this gives the wrong result in case the linked OpenSSL
|
||||||
# similarly to how cryptography does it.
|
# doesn't know the OID. That's why we have to get the OID dotted string
|
||||||
|
# similarly to how cryptography does it.
|
||||||
|
except AttributeError:
|
||||||
|
# When PyOpenSSL is used with cryptography >= 35.0.0, obj2txt cannot be used.
|
||||||
|
# We try to figure out the OID with our internal lookup table, and if we fail,
|
||||||
|
# we use the short name OpenSSL returns.
|
||||||
|
oid = to_native(ext.get_short_name())
|
||||||
|
oid = OID_LOOKUP.get(oid, oid)
|
||||||
result[oid] = entry
|
result[oid] = entry
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from ansible.module_utils import six
|
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:
|
try:
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
@@ -52,7 +52,14 @@ from .basic import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_fingerprint_of_bytes(source):
|
# This list of preferred fingerprints is used when prefer_one=True is supplied to the
|
||||||
|
# fingerprinting methods.
|
||||||
|
PREFERRED_FINGERPRINTS = (
|
||||||
|
'sha256', 'sha3_256', 'sha512', 'sha3_512', 'sha384', 'sha3_384', 'sha1', 'md5'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_fingerprint_of_bytes(source, prefer_one=False):
|
||||||
"""Generate the fingerprint of the given bytes."""
|
"""Generate the fingerprint of the given bytes."""
|
||||||
|
|
||||||
fingerprint = {}
|
fingerprint = {}
|
||||||
@@ -65,6 +72,12 @@ def get_fingerprint_of_bytes(source):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if prefer_one:
|
||||||
|
# Sort algorithms to have the ones in PREFERRED_FINGERPRINTS at the beginning
|
||||||
|
prefered_algorithms = [algorithm for algorithm in PREFERRED_FINGERPRINTS if algorithm in algorithms]
|
||||||
|
prefered_algorithms += sorted([algorithm for algorithm in algorithms if algorithm not in PREFERRED_FINGERPRINTS])
|
||||||
|
algorithms = prefered_algorithms
|
||||||
|
|
||||||
for algo in algorithms:
|
for algo in algorithms:
|
||||||
f = getattr(hashlib, algo)
|
f = getattr(hashlib, algo)
|
||||||
try:
|
try:
|
||||||
@@ -79,11 +92,13 @@ def get_fingerprint_of_bytes(source):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
pubkey_digest = h.hexdigest(32)
|
pubkey_digest = h.hexdigest(32)
|
||||||
fingerprint[algo] = ':'.join(pubkey_digest[i:i + 2] for i in range(0, len(pubkey_digest), 2))
|
fingerprint[algo] = ':'.join(pubkey_digest[i:i + 2] for i in range(0, len(pubkey_digest), 2))
|
||||||
|
if prefer_one:
|
||||||
|
break
|
||||||
|
|
||||||
return fingerprint
|
return fingerprint
|
||||||
|
|
||||||
|
|
||||||
def get_fingerprint_of_privatekey(privatekey, backend='pyopenssl'):
|
def get_fingerprint_of_privatekey(privatekey, backend='pyopenssl', prefer_one=False):
|
||||||
"""Generate the fingerprint of the public key. """
|
"""Generate the fingerprint of the public key. """
|
||||||
|
|
||||||
if backend == 'pyopenssl':
|
if backend == 'pyopenssl':
|
||||||
@@ -107,15 +122,15 @@ def get_fingerprint_of_privatekey(privatekey, backend='pyopenssl'):
|
|||||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||||
)
|
)
|
||||||
|
|
||||||
return get_fingerprint_of_bytes(publickey)
|
return get_fingerprint_of_bytes(publickey, prefer_one=prefer_one)
|
||||||
|
|
||||||
|
|
||||||
def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl'):
|
def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl', prefer_one=False):
|
||||||
"""Generate the fingerprint of the public key. """
|
"""Generate the fingerprint of the public key. """
|
||||||
|
|
||||||
privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend)
|
privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend)
|
||||||
|
|
||||||
return get_fingerprint_of_privatekey(privatekey, backend=backend)
|
return get_fingerprint_of_privatekey(privatekey, backend=backend, prefer_one=prefer_one)
|
||||||
|
|
||||||
|
|
||||||
def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='pyopenssl'):
|
def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='pyopenssl'):
|
||||||
@@ -183,6 +198,28 @@ def load_privatekey(path, passphrase=None, check_passphrase=True, content=None,
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def load_publickey(path=None, content=None, backend=None):
|
||||||
|
if content is None:
|
||||||
|
if path is None:
|
||||||
|
raise OpenSSLObjectError('Must provide either path or content')
|
||||||
|
try:
|
||||||
|
with open(path, 'rb') as b_priv_key_fh:
|
||||||
|
content = b_priv_key_fh.read()
|
||||||
|
except (IOError, OSError) as exc:
|
||||||
|
raise OpenSSLObjectError(exc)
|
||||||
|
|
||||||
|
if backend == 'cryptography':
|
||||||
|
try:
|
||||||
|
return serialization.load_pem_public_key(content, backend=cryptography_backend())
|
||||||
|
except Exception as e:
|
||||||
|
raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e))
|
||||||
|
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='pyopenssl'):
|
||||||
"""Load the specified certificate."""
|
"""Load the specified certificate."""
|
||||||
|
|
||||||
@@ -334,6 +371,8 @@ class OpenSSLObject(object):
|
|||||||
|
|
||||||
def _check_perms(module):
|
def _check_perms(module):
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
file_args = module.load_file_common_arguments(module.params)
|
||||||
|
if module.check_file_absent_if_check_mode(file_args['path']):
|
||||||
|
return False
|
||||||
return not module.set_fs_attributes_if_different(file_args, False)
|
return not module.set_fs_attributes_if_different(file_args, False)
|
||||||
|
|
||||||
if not perms_required:
|
if not perms_required:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import re
|
|||||||
import time
|
import time
|
||||||
import traceback
|
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.basic import missing_required_lib
|
||||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||||
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ def write_file(module, content, default_mode=None, path=None):
|
|||||||
# Move tempfile to final destination
|
# Move tempfile to final destination
|
||||||
module.atomic_move(tmp_name, file_args['path'])
|
module.atomic_move(tmp_name, file_args['path'])
|
||||||
# Try to update permissions again
|
# Try to update permissions again
|
||||||
module.set_fs_attributes_if_different(file_args, False)
|
if not module.check_file_absent_if_check_mode(file_args['path']):
|
||||||
|
module.set_fs_attributes_if_different(file_args, False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
try:
|
try:
|
||||||
os.remove(tmp_name)
|
os.remove(tmp_name)
|
||||||
|
|||||||
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,
|
||||||
|
}
|
||||||
464
plugins/module_utils/openssh/backends/keypair_backend.py
Normal file
464
plugins/module_utils/openssh/backends/keypair_backend.py
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright: (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at>
|
||||||
|
# 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
|
||||||
|
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.cryptography import (
|
||||||
|
HAS_OPENSSH_SUPPORT,
|
||||||
|
HAS_OPENSSH_PRIVATE_FORMAT,
|
||||||
|
InvalidCommentError,
|
||||||
|
InvalidPassphraseError,
|
||||||
|
InvalidPrivateKeyFileError,
|
||||||
|
OpenSSHError,
|
||||||
|
OpensshKeypair,
|
||||||
|
)
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import (
|
||||||
|
KeygenCommand,
|
||||||
|
OpensshModule,
|
||||||
|
PrivateKey,
|
||||||
|
PublicKey,
|
||||||
|
)
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
|
||||||
|
any_in,
|
||||||
|
file_mode,
|
||||||
|
secure_write,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class KeypairBackend(OpensshModule):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
super(KeypairBackend, self).__init__(module)
|
||||||
|
|
||||||
|
self.comment = self.module.params['comment']
|
||||||
|
self.private_key_path = self.module.params['path']
|
||||||
|
self.public_key_path = self.private_key_path + '.pub'
|
||||||
|
self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always'
|
||||||
|
self.state = self.module.params['state']
|
||||||
|
self.type = self.module.params['type']
|
||||||
|
|
||||||
|
self.size = self._get_size(self.module.params['size'])
|
||||||
|
self._validate_path()
|
||||||
|
|
||||||
|
self.original_private_key = None
|
||||||
|
self.original_public_key = None
|
||||||
|
self.private_key = None
|
||||||
|
self.public_key = None
|
||||||
|
|
||||||
|
def _get_size(self, size):
|
||||||
|
if self.type in ('rsa', 'rsa1'):
|
||||||
|
result = 4096 if size is None else size
|
||||||
|
if result < 1024:
|
||||||
|
return self.module.fail_json(
|
||||||
|
msg="For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. " +
|
||||||
|
"Attempting to use bit lengths under 1024 will cause the module to fail."
|
||||||
|
)
|
||||||
|
elif self.type == 'dsa':
|
||||||
|
result = 1024 if size is None else size
|
||||||
|
if result != 1024:
|
||||||
|
return self.module.fail_json(msg="DSA keys must be exactly 1024 bits as specified by FIPS 186-2.")
|
||||||
|
elif self.type == 'ecdsa':
|
||||||
|
result = 256 if size is None else size
|
||||||
|
if result not in (256, 384, 521):
|
||||||
|
return self.module.fail_json(
|
||||||
|
msg="For ECDSA keys, size determines the key length by selecting from one of " +
|
||||||
|
"three elliptic curve sizes: 256, 384 or 521 bits. " +
|
||||||
|
"Attempting to use bit lengths other than these three values for ECDSA keys will " +
|
||||||
|
"cause this module to fail."
|
||||||
|
)
|
||||||
|
elif self.type == 'ed25519':
|
||||||
|
# User input is ignored for `key size` when `key type` is ed25519
|
||||||
|
result = 256
|
||||||
|
else:
|
||||||
|
return self.module.fail_json(msg="%s is not a valid value for key type" % self.type)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _validate_path(self):
|
||||||
|
self._check_if_base_dir(self.private_key_path)
|
||||||
|
|
||||||
|
if os.path.isdir(self.private_key_path):
|
||||||
|
self.module.fail_json(msg='%s is a directory. Please specify a path to a file.' % self.private_key_path)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.original_private_key = self._load_private_key()
|
||||||
|
self.original_public_key = self._load_public_key()
|
||||||
|
|
||||||
|
if self.state == 'present':
|
||||||
|
self._validate_key_load()
|
||||||
|
|
||||||
|
if self._should_generate():
|
||||||
|
self._generate()
|
||||||
|
elif not self._public_key_valid():
|
||||||
|
self._restore_public_key()
|
||||||
|
|
||||||
|
self.private_key = self._load_private_key()
|
||||||
|
self.public_key = self._load_public_key()
|
||||||
|
|
||||||
|
for path in (self.private_key_path, self.public_key_path):
|
||||||
|
self._update_permissions(path)
|
||||||
|
else:
|
||||||
|
if self._should_remove():
|
||||||
|
self._remove()
|
||||||
|
|
||||||
|
def _load_private_key(self):
|
||||||
|
result = None
|
||||||
|
if self._private_key_exists():
|
||||||
|
try:
|
||||||
|
result = self._get_private_key()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _private_key_exists(self):
|
||||||
|
return os.path.exists(self.private_key_path)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_private_key(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _load_public_key(self):
|
||||||
|
result = None
|
||||||
|
if self._public_key_exists():
|
||||||
|
try:
|
||||||
|
result = PublicKey.load(self.public_key_path)
|
||||||
|
except (IOError, OSError):
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _public_key_exists(self):
|
||||||
|
return os.path.exists(self.public_key_path)
|
||||||
|
|
||||||
|
def _validate_key_load(self):
|
||||||
|
if (self._private_key_exists()
|
||||||
|
and self.regenerate in ('never', 'fail', 'partial_idempotence')
|
||||||
|
and (self.original_private_key is None or not self._private_key_readable())):
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="Unable to read the key. The key is protected with a passphrase or broken. " +
|
||||||
|
"Will not proceed. To force regeneration, call the module with `generate` " +
|
||||||
|
"set to `full_idempotence` or `always`, or with `force=yes`."
|
||||||
|
)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
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 _generate_keypair(self, private_key_path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _public_key_valid(self):
|
||||||
|
if self.original_public_key is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
valid_public_key = self._get_public_key()
|
||||||
|
valid_public_key.comment = self.comment
|
||||||
|
|
||||||
|
return self.original_public_key == valid_public_key
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_public_key(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@OpensshModule.trigger_change
|
||||||
|
@OpensshModule.skip_if_check_mode
|
||||||
|
def _restore_public_key(self):
|
||||||
|
try:
|
||||||
|
temp_public_key = self._create_temp_public_key(str(self._get_public_key()) + '\n')
|
||||||
|
self._safe_secure_move([
|
||||||
|
(temp_public_key, self.public_key_path)
|
||||||
|
])
|
||||||
|
except (IOError, OSError):
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="The public key is missing or does not match the private key. " +
|
||||||
|
"Unable to regenerate the public key."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.comment:
|
||||||
|
self._update_comment()
|
||||||
|
|
||||||
|
def _create_temp_public_key(self, content):
|
||||||
|
temp_public_key = os.path.join(self.module.tmpdir, os.path.basename(self.public_key_path))
|
||||||
|
|
||||||
|
default_permissions = 0o644
|
||||||
|
existing_permissions = file_mode(self.public_key_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
secure_write(temp_public_key, existing_permissions or default_permissions, to_bytes(content))
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
self.module.fail_json(msg=to_native(e))
|
||||||
|
self.module.add_cleanup_file(temp_public_key)
|
||||||
|
|
||||||
|
return temp_public_key
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _update_comment(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _should_remove(self):
|
||||||
|
return self._private_key_exists() or self._public_key_exists()
|
||||||
|
|
||||||
|
@OpensshModule.trigger_change
|
||||||
|
@OpensshModule.skip_if_check_mode
|
||||||
|
def _remove(self):
|
||||||
|
try:
|
||||||
|
if self._private_key_exists():
|
||||||
|
os.remove(self.private_key_path)
|
||||||
|
if self._public_key_exists():
|
||||||
|
os.remove(self.public_key_path)
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
self.module.fail_json(msg=to_native(e))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _result(self):
|
||||||
|
private_key = self.private_key or self.original_private_key
|
||||||
|
public_key = self.public_key or self.original_public_key
|
||||||
|
|
||||||
|
return {
|
||||||
|
'size': self.size,
|
||||||
|
'type': self.type,
|
||||||
|
'filename': self.private_key_path,
|
||||||
|
'fingerprint': private_key.fingerprint if private_key else '',
|
||||||
|
'public_key': str(public_key) if public_key else '',
|
||||||
|
'comment': public_key.comment if public_key else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def diff(self):
|
||||||
|
before = self.original_private_key.to_dict() if self.original_private_key else {}
|
||||||
|
before.update(self.original_public_key.to_dict() if self.original_public_key else {})
|
||||||
|
|
||||||
|
after = self.private_key.to_dict() if self.private_key else {}
|
||||||
|
after.update(self.public_key.to_dict() if self.public_key else {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'before': before,
|
||||||
|
'after': after,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class KeypairBackendOpensshBin(KeypairBackend):
|
||||||
|
def __init__(self, module):
|
||||||
|
super(KeypairBackendOpensshBin, self).__init__(module)
|
||||||
|
|
||||||
|
self.ssh_keygen = KeygenCommand(self.module)
|
||||||
|
|
||||||
|
def _generate_keypair(self, private_key_path):
|
||||||
|
self.ssh_keygen.generate_keypair(private_key_path, self.size, self.type, self.comment)
|
||||||
|
|
||||||
|
def _get_private_key(self):
|
||||||
|
private_key_content = self.ssh_keygen.get_private_key(self.private_key_path)[1]
|
||||||
|
return PrivateKey.from_string(private_key_content)
|
||||||
|
|
||||||
|
def _get_public_key(self):
|
||||||
|
public_key_content = self.ssh_keygen.get_matching_public_key(self.private_key_path)[1]
|
||||||
|
return PublicKey.from_string(public_key_content)
|
||||||
|
|
||||||
|
def _private_key_readable(self):
|
||||||
|
rc, stdout, stderr = self.ssh_keygen.get_matching_public_key(self.private_key_path)
|
||||||
|
return not (rc == 255 or any_in(stderr, 'is not a public key file', 'incorrect passphrase', 'load failed'))
|
||||||
|
|
||||||
|
def _update_comment(self):
|
||||||
|
try:
|
||||||
|
self.ssh_keygen.update_comment(self.private_key_path, self.comment)
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
self.module.fail_json(msg=to_native(e))
|
||||||
|
|
||||||
|
|
||||||
|
class KeypairBackendCryptography(KeypairBackend):
|
||||||
|
def __init__(self, module):
|
||||||
|
super(KeypairBackendCryptography, self).__init__(module)
|
||||||
|
|
||||||
|
if self.type == 'rsa1':
|
||||||
|
self.module.fail_json(msg="RSA1 keys are not supported by the cryptography backend")
|
||||||
|
|
||||||
|
self.passphrase = to_bytes(module.params['passphrase']) if module.params['passphrase'] else None
|
||||||
|
self.private_key_format = self._get_key_format(module.params['private_key_format'])
|
||||||
|
|
||||||
|
def _get_key_format(self, key_format):
|
||||||
|
result = 'SSH'
|
||||||
|
|
||||||
|
if key_format == 'auto':
|
||||||
|
# Default to OpenSSH 7.8 compatibility when OpenSSH is not installed
|
||||||
|
ssh_version = self._get_ssh_version() or "7.8"
|
||||||
|
|
||||||
|
if LooseVersion(ssh_version) < LooseVersion("7.8") and self.type != 'ed25519':
|
||||||
|
# OpenSSH made SSH formatted private keys available in version 6.5,
|
||||||
|
# but still defaulted to PKCS1 format with the exception of ed25519 keys
|
||||||
|
result = 'PKCS1'
|
||||||
|
|
||||||
|
if result == 'SSH' and not HAS_OPENSSH_PRIVATE_FORMAT:
|
||||||
|
self.module.fail_json(
|
||||||
|
msg=missing_required_lib(
|
||||||
|
'cryptography >= 3.0',
|
||||||
|
reason="to load/dump private keys in the default OpenSSH format for OpenSSH >= 7.8 " +
|
||||||
|
"or for ed25519 keys"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _generate_keypair(self, private_key_path):
|
||||||
|
keypair = OpensshKeypair.generate(
|
||||||
|
keytype=self.type,
|
||||||
|
size=self.size,
|
||||||
|
passphrase=self.passphrase,
|
||||||
|
comment=self.comment or '',
|
||||||
|
)
|
||||||
|
|
||||||
|
encoded_private_key = OpensshKeypair.encode_openssh_privatekey(
|
||||||
|
keypair.asymmetric_keypair, self.private_key_format
|
||||||
|
)
|
||||||
|
secure_write(private_key_path, 0o600, encoded_private_key)
|
||||||
|
|
||||||
|
public_key_path = private_key_path + '.pub'
|
||||||
|
secure_write(public_key_path, 0o644, keypair.public_key)
|
||||||
|
|
||||||
|
def _get_private_key(self):
|
||||||
|
keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
|
||||||
|
|
||||||
|
return PrivateKey(
|
||||||
|
size=keypair.size,
|
||||||
|
key_type=keypair.key_type,
|
||||||
|
fingerprint=keypair.fingerprint,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_public_key(self):
|
||||||
|
try:
|
||||||
|
keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
|
||||||
|
except OpenSSHError:
|
||||||
|
# Simulates the null output of ssh-keygen
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return PublicKey.from_string(to_text(keypair.public_key))
|
||||||
|
|
||||||
|
def _private_key_readable(self):
|
||||||
|
try:
|
||||||
|
OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
|
||||||
|
except (InvalidPrivateKeyFileError, InvalidPassphraseError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Cryptography >= 3.0 uses a SSH key loader which does not raise an exception when a passphrase is provided
|
||||||
|
# when loading an unencrypted key
|
||||||
|
if self.passphrase:
|
||||||
|
try:
|
||||||
|
OpensshKeypair.load(path=self.private_key_path, passphrase=None, no_public_key=True)
|
||||||
|
except (InvalidPrivateKeyFileError, InvalidPassphraseError):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _update_comment(self):
|
||||||
|
keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
|
||||||
|
try:
|
||||||
|
keypair.comment = self.comment
|
||||||
|
except InvalidCommentError as e:
|
||||||
|
self.module.fail_json(msg=to_native(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
temp_public_key = self._create_temp_public_key(keypair.public_key + b'\n')
|
||||||
|
self._safe_secure_move([(temp_public_key, self.public_key_path)])
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
self.module.fail_json(msg=to_native(e))
|
||||||
|
|
||||||
|
|
||||||
|
def select_backend(module, backend):
|
||||||
|
can_use_cryptography = HAS_OPENSSH_SUPPORT
|
||||||
|
can_use_opensshbin = bool(module.get_bin_path('ssh-keygen'))
|
||||||
|
|
||||||
|
if backend == 'auto':
|
||||||
|
if can_use_opensshbin and not module.params['passphrase']:
|
||||||
|
backend = 'opensshbin'
|
||||||
|
elif can_use_cryptography:
|
||||||
|
backend = 'cryptography'
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="Cannot find either the OpenSSH binary in the PATH " +
|
||||||
|
"or cryptography >= 2.6 installed on this system")
|
||||||
|
|
||||||
|
if backend == 'opensshbin':
|
||||||
|
if not can_use_opensshbin:
|
||||||
|
module.fail_json(msg="Cannot find the OpenSSH binary in the PATH")
|
||||||
|
return backend, KeypairBackendOpensshBin(module)
|
||||||
|
elif backend == 'cryptography':
|
||||||
|
if not can_use_cryptography:
|
||||||
|
module.fail_json(msg=missing_required_lib("cryptography >= 2.6"))
|
||||||
|
return backend, KeypairBackendCryptography(module)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||||
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)))
|
||||||
695
plugins/module_utils/openssh/cryptography.py
Normal file
695
plugins/module_utils/openssh/cryptography.py
Normal file
@@ -0,0 +1,695 @@
|
|||||||
|
# -*- 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 os
|
||||||
|
from base64 import b64encode, b64decode
|
||||||
|
from distutils.version import LooseVersion
|
||||||
|
from getpass import getuser
|
||||||
|
from socket import gethostname
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cryptography import __version__ as CRYPTOGRAPHY_VERSION
|
||||||
|
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
|
||||||
|
from cryptography.hazmat.backends.openssl import backend
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa, padding
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||||
|
|
||||||
|
if LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion("3.0"):
|
||||||
|
HAS_OPENSSH_PRIVATE_FORMAT = True
|
||||||
|
else:
|
||||||
|
HAS_OPENSSH_PRIVATE_FORMAT = False
|
||||||
|
|
||||||
|
HAS_OPENSSH_SUPPORT = True
|
||||||
|
|
||||||
|
_ALGORITHM_PARAMETERS = {
|
||||||
|
'rsa': {
|
||||||
|
'default_size': 2048,
|
||||||
|
'valid_sizes': range(1024, 16384),
|
||||||
|
'signer_params': {
|
||||||
|
'padding': padding.PSS(
|
||||||
|
mgf=padding.MGF1(hashes.SHA256()),
|
||||||
|
salt_length=padding.PSS.MAX_LENGTH,
|
||||||
|
),
|
||||||
|
'algorithm': hashes.SHA256(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'dsa': {
|
||||||
|
'default_size': 1024,
|
||||||
|
'valid_sizes': [1024],
|
||||||
|
'signer_params': {
|
||||||
|
'algorithm': hashes.SHA256(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'ed25519': {
|
||||||
|
'default_size': 256,
|
||||||
|
'valid_sizes': [256],
|
||||||
|
'signer_params': {},
|
||||||
|
},
|
||||||
|
'ecdsa': {
|
||||||
|
'default_size': 256,
|
||||||
|
'valid_sizes': [256, 384, 521],
|
||||||
|
'signer_params': {
|
||||||
|
'signature_algorithm': ec.ECDSA(hashes.SHA256()),
|
||||||
|
},
|
||||||
|
'curves': {
|
||||||
|
256: ec.SECP256R1(),
|
||||||
|
384: ec.SECP384R1(),
|
||||||
|
521: ec.SECP521R1(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except ImportError:
|
||||||
|
HAS_OPENSSH_PRIVATE_FORMAT = False
|
||||||
|
HAS_OPENSSH_SUPPORT = False
|
||||||
|
CRYPTOGRAPHY_VERSION = "0.0"
|
||||||
|
_ALGORITHM_PARAMETERS = {}
|
||||||
|
|
||||||
|
_TEXT_ENCODING = 'UTF-8'
|
||||||
|
|
||||||
|
|
||||||
|
class OpenSSHError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAlgorithmError(OpenSSHError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidCommentError(OpenSSHError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidDataError(OpenSSHError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPrivateKeyFileError(OpenSSHError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPublicKeyFileError(OpenSSHError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidKeyFormatError(OpenSSHError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidKeySizeError(OpenSSHError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidKeyTypeError(OpenSSHError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPassphraseError(OpenSSHError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSignatureError(OpenSSHError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AsymmetricKeypair(object):
|
||||||
|
"""Container for newly generated asymmetric key pairs or those loaded from existing files"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate(cls, keytype='rsa', size=None, passphrase=None):
|
||||||
|
"""Returns an Asymmetric_Keypair object generated with the supplied parameters
|
||||||
|
or defaults to an unencrypted RSA-2048 key
|
||||||
|
|
||||||
|
:keytype: One of rsa, dsa, ecdsa, ed25519
|
||||||
|
:size: The key length for newly generated keys
|
||||||
|
:passphrase: Secret of type Bytes used to encrypt the private key being generated
|
||||||
|
"""
|
||||||
|
|
||||||
|
if keytype not in _ALGORITHM_PARAMETERS.keys():
|
||||||
|
raise InvalidKeyTypeError(
|
||||||
|
"%s is not a valid keytype. Valid keytypes are %s" % (
|
||||||
|
keytype, ", ".join(_ALGORITHM_PARAMETERS.keys())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not size:
|
||||||
|
size = _ALGORITHM_PARAMETERS[keytype]['default_size']
|
||||||
|
else:
|
||||||
|
if size not in _ALGORITHM_PARAMETERS[keytype]['valid_sizes']:
|
||||||
|
raise InvalidKeySizeError(
|
||||||
|
"%s is not a valid key size for %s keys" % (size, keytype)
|
||||||
|
)
|
||||||
|
|
||||||
|
if passphrase:
|
||||||
|
encryption_algorithm = get_encryption_algorithm(passphrase)
|
||||||
|
else:
|
||||||
|
encryption_algorithm = serialization.NoEncryption()
|
||||||
|
|
||||||
|
if keytype == 'rsa':
|
||||||
|
privatekey = rsa.generate_private_key(
|
||||||
|
# Public exponent should always be 65537 to prevent issues
|
||||||
|
# if improper padding is used during signing
|
||||||
|
public_exponent=65537,
|
||||||
|
key_size=size,
|
||||||
|
backend=backend,
|
||||||
|
)
|
||||||
|
elif keytype == 'dsa':
|
||||||
|
privatekey = dsa.generate_private_key(
|
||||||
|
key_size=size,
|
||||||
|
backend=backend,
|
||||||
|
)
|
||||||
|
elif keytype == 'ed25519':
|
||||||
|
privatekey = Ed25519PrivateKey.generate()
|
||||||
|
elif keytype == 'ecdsa':
|
||||||
|
privatekey = ec.generate_private_key(
|
||||||
|
_ALGORITHM_PARAMETERS['ecdsa']['curves'][size],
|
||||||
|
backend=backend,
|
||||||
|
)
|
||||||
|
|
||||||
|
publickey = privatekey.public_key()
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
keytype=keytype,
|
||||||
|
size=size,
|
||||||
|
privatekey=privatekey,
|
||||||
|
publickey=publickey,
|
||||||
|
encryption_algorithm=encryption_algorithm
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, path, passphrase=None, private_key_format='PEM', public_key_format='PEM', no_public_key=False):
|
||||||
|
"""Returns an Asymmetric_Keypair object loaded from the supplied file path
|
||||||
|
|
||||||
|
:path: A path to an existing private key to be loaded
|
||||||
|
:passphrase: Secret of type bytes used to decrypt the private key being loaded
|
||||||
|
:private_key_format: Format of private key to be loaded
|
||||||
|
:public_key_format: Format of public key to be loaded
|
||||||
|
:no_public_key: Set 'True' to only load a private key and automatically populate the matching public key
|
||||||
|
"""
|
||||||
|
|
||||||
|
if passphrase:
|
||||||
|
encryption_algorithm = get_encryption_algorithm(passphrase)
|
||||||
|
else:
|
||||||
|
encryption_algorithm = serialization.NoEncryption()
|
||||||
|
|
||||||
|
privatekey = load_privatekey(path, passphrase, private_key_format)
|
||||||
|
if no_public_key:
|
||||||
|
publickey = privatekey.public_key()
|
||||||
|
else:
|
||||||
|
publickey = load_publickey(path + '.pub', public_key_format)
|
||||||
|
|
||||||
|
# Ed25519 keys are always of size 256 and do not have a key_size attribute
|
||||||
|
if isinstance(privatekey, Ed25519PrivateKey):
|
||||||
|
size = _ALGORITHM_PARAMETERS['ed25519']['default_size']
|
||||||
|
else:
|
||||||
|
size = privatekey.key_size
|
||||||
|
|
||||||
|
if isinstance(privatekey, rsa.RSAPrivateKey):
|
||||||
|
keytype = 'rsa'
|
||||||
|
elif isinstance(privatekey, dsa.DSAPrivateKey):
|
||||||
|
keytype = 'dsa'
|
||||||
|
elif isinstance(privatekey, ec.EllipticCurvePrivateKey):
|
||||||
|
keytype = 'ecdsa'
|
||||||
|
elif isinstance(privatekey, Ed25519PrivateKey):
|
||||||
|
keytype = 'ed25519'
|
||||||
|
else:
|
||||||
|
raise InvalidKeyTypeError("Key type '%s' is not supported" % type(privatekey))
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
keytype=keytype,
|
||||||
|
size=size,
|
||||||
|
privatekey=privatekey,
|
||||||
|
publickey=publickey,
|
||||||
|
encryption_algorithm=encryption_algorithm
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, keytype, size, privatekey, publickey, encryption_algorithm):
|
||||||
|
"""
|
||||||
|
:keytype: One of rsa, dsa, ecdsa, ed25519
|
||||||
|
:size: The key length for the private key of this key pair
|
||||||
|
:privatekey: Private key object of this key pair
|
||||||
|
:publickey: Public key object of this key pair
|
||||||
|
:encryption_algorithm: Hashed secret used to encrypt the private key of this key pair
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.__size = size
|
||||||
|
self.__keytype = keytype
|
||||||
|
self.__privatekey = privatekey
|
||||||
|
self.__publickey = publickey
|
||||||
|
self.__encryption_algorithm = encryption_algorithm
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.verify(self.sign(b'message'), b'message')
|
||||||
|
except InvalidSignatureError:
|
||||||
|
raise InvalidPublicKeyFileError(
|
||||||
|
"The private key and public key of this keypair do not match"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, AsymmetricKeypair):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return (compare_publickeys(self.public_key, other.public_key) and
|
||||||
|
compare_encryption_algorithms(self.encryption_algorithm, other.encryption_algorithm))
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self == other
|
||||||
|
|
||||||
|
@property
|
||||||
|
def private_key(self):
|
||||||
|
"""Returns the private key of this key pair"""
|
||||||
|
|
||||||
|
return self.__privatekey
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_key(self):
|
||||||
|
"""Returns the public key of this key pair"""
|
||||||
|
|
||||||
|
return self.__publickey
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self):
|
||||||
|
"""Returns the size of the private key of this key pair"""
|
||||||
|
|
||||||
|
return self.__size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key_type(self):
|
||||||
|
"""Returns the key type of this key pair"""
|
||||||
|
|
||||||
|
return self.__keytype
|
||||||
|
|
||||||
|
@property
|
||||||
|
def encryption_algorithm(self):
|
||||||
|
"""Returns the key encryption algorithm of this key pair"""
|
||||||
|
|
||||||
|
return self.__encryption_algorithm
|
||||||
|
|
||||||
|
def sign(self, data):
|
||||||
|
"""Returns signature of data signed with the private key of this key pair
|
||||||
|
|
||||||
|
:data: byteslike data to sign
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
signature = self.__privatekey.sign(
|
||||||
|
data,
|
||||||
|
**_ALGORITHM_PARAMETERS[self.__keytype]['signer_params']
|
||||||
|
)
|
||||||
|
except TypeError as e:
|
||||||
|
raise InvalidDataError(e)
|
||||||
|
|
||||||
|
return signature
|
||||||
|
|
||||||
|
def verify(self, signature, data):
|
||||||
|
"""Verifies that the signature associated with the provided data was signed
|
||||||
|
by the private key of this key pair.
|
||||||
|
|
||||||
|
:signature: signature to verify
|
||||||
|
:data: byteslike data signed by the provided signature
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.__publickey.verify(
|
||||||
|
signature,
|
||||||
|
data,
|
||||||
|
**_ALGORITHM_PARAMETERS[self.__keytype]['signer_params']
|
||||||
|
)
|
||||||
|
except InvalidSignature:
|
||||||
|
raise InvalidSignatureError
|
||||||
|
|
||||||
|
def update_passphrase(self, passphrase=None):
|
||||||
|
"""Updates the encryption algorithm of this key pair
|
||||||
|
|
||||||
|
:passphrase: Byte secret used to encrypt this key pair
|
||||||
|
"""
|
||||||
|
|
||||||
|
if passphrase:
|
||||||
|
self.__encryption_algorithm = get_encryption_algorithm(passphrase)
|
||||||
|
else:
|
||||||
|
self.__encryption_algorithm = serialization.NoEncryption()
|
||||||
|
|
||||||
|
|
||||||
|
class OpensshKeypair(object):
|
||||||
|
"""Container for OpenSSH encoded asymmetric key pairs"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate(cls, keytype='rsa', size=None, passphrase=None, comment=None):
|
||||||
|
"""Returns an Openssh_Keypair object generated using the supplied parameters or defaults to a RSA-2048 key
|
||||||
|
|
||||||
|
:keytype: One of rsa, dsa, ecdsa, ed25519
|
||||||
|
:size: The key length for newly generated keys
|
||||||
|
:passphrase: Secret of type Bytes used to encrypt the newly generated private key
|
||||||
|
:comment: Comment for a newly generated OpenSSH public key
|
||||||
|
"""
|
||||||
|
|
||||||
|
if comment is None:
|
||||||
|
comment = "%s@%s" % (getuser(), gethostname())
|
||||||
|
|
||||||
|
asym_keypair = AsymmetricKeypair.generate(keytype, size, passphrase)
|
||||||
|
openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, 'SSH')
|
||||||
|
openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment)
|
||||||
|
fingerprint = calculate_fingerprint(openssh_publickey)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
asym_keypair=asym_keypair,
|
||||||
|
openssh_privatekey=openssh_privatekey,
|
||||||
|
openssh_publickey=openssh_publickey,
|
||||||
|
fingerprint=fingerprint,
|
||||||
|
comment=comment,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, path, passphrase=None, no_public_key=False):
|
||||||
|
"""Returns an Openssh_Keypair object loaded from the supplied file path
|
||||||
|
|
||||||
|
:path: A path to an existing private key to be loaded
|
||||||
|
:passphrase: Secret used to decrypt the private key being loaded
|
||||||
|
:no_public_key: Set 'True' to only load a private key and automatically populate the matching public key
|
||||||
|
"""
|
||||||
|
|
||||||
|
if no_public_key:
|
||||||
|
comment = ""
|
||||||
|
else:
|
||||||
|
comment = extract_comment(path + '.pub')
|
||||||
|
|
||||||
|
asym_keypair = AsymmetricKeypair.load(path, passphrase, 'SSH', 'SSH', no_public_key)
|
||||||
|
openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, 'SSH')
|
||||||
|
openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment)
|
||||||
|
fingerprint = calculate_fingerprint(openssh_publickey)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
asym_keypair=asym_keypair,
|
||||||
|
openssh_privatekey=openssh_privatekey,
|
||||||
|
openssh_publickey=openssh_publickey,
|
||||||
|
fingerprint=fingerprint,
|
||||||
|
comment=comment,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode_openssh_privatekey(asym_keypair, key_format):
|
||||||
|
"""Returns an OpenSSH encoded private key for a given keypair
|
||||||
|
|
||||||
|
:asym_keypair: Asymmetric_Keypair from the private key is extracted
|
||||||
|
:key_format: Format of the encoded private key.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if key_format == 'SSH':
|
||||||
|
# Default to PEM format if SSH not available
|
||||||
|
if not HAS_OPENSSH_PRIVATE_FORMAT:
|
||||||
|
privatekey_format = serialization.PrivateFormat.PKCS8
|
||||||
|
else:
|
||||||
|
privatekey_format = serialization.PrivateFormat.OpenSSH
|
||||||
|
elif key_format == 'PKCS8':
|
||||||
|
privatekey_format = serialization.PrivateFormat.PKCS8
|
||||||
|
elif key_format == 'PKCS1':
|
||||||
|
if asym_keypair.key_type == 'ed25519':
|
||||||
|
raise InvalidKeyFormatError("ed25519 keys cannot be represented in PKCS1 format")
|
||||||
|
privatekey_format = serialization.PrivateFormat.TraditionalOpenSSL
|
||||||
|
else:
|
||||||
|
raise InvalidKeyFormatError("The accepted private key formats are SSH, PKCS8, and PKCS1")
|
||||||
|
|
||||||
|
encoded_privatekey = asym_keypair.private_key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=privatekey_format,
|
||||||
|
encryption_algorithm=asym_keypair.encryption_algorithm
|
||||||
|
)
|
||||||
|
|
||||||
|
return encoded_privatekey
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode_openssh_publickey(asym_keypair, comment):
|
||||||
|
"""Returns an OpenSSH encoded public key for a given keypair
|
||||||
|
|
||||||
|
:asym_keypair: Asymmetric_Keypair from the public key is extracted
|
||||||
|
:comment: Comment to apply to the end of the returned OpenSSH encoded public key
|
||||||
|
"""
|
||||||
|
encoded_publickey = asym_keypair.public_key.public_bytes(
|
||||||
|
encoding=serialization.Encoding.OpenSSH,
|
||||||
|
format=serialization.PublicFormat.OpenSSH,
|
||||||
|
)
|
||||||
|
|
||||||
|
validate_comment(comment)
|
||||||
|
|
||||||
|
encoded_publickey += (" %s" % comment).encode(encoding=_TEXT_ENCODING) if comment else b''
|
||||||
|
|
||||||
|
return encoded_publickey
|
||||||
|
|
||||||
|
def __init__(self, asym_keypair, openssh_privatekey, openssh_publickey, fingerprint, comment):
|
||||||
|
"""
|
||||||
|
:asym_keypair: An Asymmetric_Keypair object from which the OpenSSH encoded keypair is derived
|
||||||
|
:openssh_privatekey: An OpenSSH encoded private key
|
||||||
|
:openssh_privatekey: An OpenSSH encoded public key
|
||||||
|
:fingerprint: The fingerprint of the OpenSSH encoded public key of this keypair
|
||||||
|
:comment: Comment applied to the OpenSSH public key of this keypair
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.__asym_keypair = asym_keypair
|
||||||
|
self.__openssh_privatekey = openssh_privatekey
|
||||||
|
self.__openssh_publickey = openssh_publickey
|
||||||
|
self.__fingerprint = fingerprint
|
||||||
|
self.__comment = comment
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, OpensshKeypair):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return self.asymmetric_keypair == other.asymmetric_keypair and self.comment == other.comment
|
||||||
|
|
||||||
|
@property
|
||||||
|
def asymmetric_keypair(self):
|
||||||
|
"""Returns the underlying asymmetric key pair of this OpenSSH encoded key pair"""
|
||||||
|
|
||||||
|
return self.__asym_keypair
|
||||||
|
|
||||||
|
@property
|
||||||
|
def private_key(self):
|
||||||
|
"""Returns the OpenSSH formatted private key of this key pair"""
|
||||||
|
|
||||||
|
return self.__openssh_privatekey
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_key(self):
|
||||||
|
"""Returns the OpenSSH formatted public key of this key pair"""
|
||||||
|
|
||||||
|
return self.__openssh_publickey
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self):
|
||||||
|
"""Returns the size of the private key of this key pair"""
|
||||||
|
|
||||||
|
return self.__asym_keypair.size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key_type(self):
|
||||||
|
"""Returns the key type of this key pair"""
|
||||||
|
|
||||||
|
return self.__asym_keypair.key_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fingerprint(self):
|
||||||
|
"""Returns the fingerprint (SHA256 Hash) of the public key of this key pair"""
|
||||||
|
|
||||||
|
return self.__fingerprint
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comment(self):
|
||||||
|
"""Returns the comment applied to the OpenSSH formatted public key of this key pair"""
|
||||||
|
|
||||||
|
return self.__comment
|
||||||
|
|
||||||
|
@comment.setter
|
||||||
|
def comment(self, comment):
|
||||||
|
"""Updates the comment applied to the OpenSSH formatted public key of this key pair
|
||||||
|
|
||||||
|
:comment: Text to update the OpenSSH public key comment
|
||||||
|
"""
|
||||||
|
|
||||||
|
validate_comment(comment)
|
||||||
|
|
||||||
|
self.__comment = comment
|
||||||
|
encoded_comment = (" %s" % self.__comment).encode(encoding=_TEXT_ENCODING) if self.__comment else b''
|
||||||
|
self.__openssh_publickey = b' '.join(self.__openssh_publickey.split(b' ', 2)[:2]) + encoded_comment
|
||||||
|
return self.__openssh_publickey
|
||||||
|
|
||||||
|
def update_passphrase(self, passphrase):
|
||||||
|
"""Updates the passphrase used to encrypt the private key of this keypair
|
||||||
|
|
||||||
|
:passphrase: Text secret used for encryption
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.__asym_keypair.update_passphrase(passphrase)
|
||||||
|
self.__openssh_privatekey = OpensshKeypair.encode_openssh_privatekey(self.__asym_keypair, 'SSH')
|
||||||
|
|
||||||
|
|
||||||
|
def load_privatekey(path, passphrase, key_format):
|
||||||
|
privatekey_loaders = {
|
||||||
|
'PEM': serialization.load_pem_private_key,
|
||||||
|
'DER': serialization.load_der_private_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
# OpenSSH formatted private keys are not available in Cryptography <3.0
|
||||||
|
if hasattr(serialization, 'load_ssh_private_key'):
|
||||||
|
privatekey_loaders['SSH'] = serialization.load_ssh_private_key
|
||||||
|
else:
|
||||||
|
privatekey_loaders['SSH'] = serialization.load_pem_private_key
|
||||||
|
|
||||||
|
try:
|
||||||
|
privatekey_loader = privatekey_loaders[key_format]
|
||||||
|
except KeyError:
|
||||||
|
raise InvalidKeyFormatError(
|
||||||
|
"%s is not a valid key format (%s)" % (
|
||||||
|
key_format,
|
||||||
|
','.join(privatekey_loaders.keys())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise InvalidPrivateKeyFileError("No file was found at %s" % path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
privatekey = privatekey_loader(
|
||||||
|
data=content,
|
||||||
|
password=passphrase,
|
||||||
|
backend=backend,
|
||||||
|
)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
# Revert to PEM if key could not be loaded in SSH format
|
||||||
|
if key_format == 'SSH':
|
||||||
|
try:
|
||||||
|
privatekey = privatekey_loaders['PEM'](
|
||||||
|
data=content,
|
||||||
|
password=passphrase,
|
||||||
|
backend=backend,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
raise InvalidPrivateKeyFileError(e)
|
||||||
|
except TypeError as e:
|
||||||
|
raise InvalidPassphraseError(e)
|
||||||
|
except UnsupportedAlgorithm as e:
|
||||||
|
raise InvalidAlgorithmError(e)
|
||||||
|
else:
|
||||||
|
raise InvalidPrivateKeyFileError(e)
|
||||||
|
except TypeError as e:
|
||||||
|
raise InvalidPassphraseError(e)
|
||||||
|
except UnsupportedAlgorithm as e:
|
||||||
|
raise InvalidAlgorithmError(e)
|
||||||
|
|
||||||
|
return privatekey
|
||||||
|
|
||||||
|
|
||||||
|
def load_publickey(path, key_format):
|
||||||
|
publickey_loaders = {
|
||||||
|
'PEM': serialization.load_pem_public_key,
|
||||||
|
'DER': serialization.load_der_public_key,
|
||||||
|
'SSH': serialization.load_ssh_public_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
publickey_loader = publickey_loaders[key_format]
|
||||||
|
except KeyError:
|
||||||
|
raise InvalidKeyFormatError(
|
||||||
|
"%s is not a valid key format (%s)" % (
|
||||||
|
key_format,
|
||||||
|
','.join(publickey_loaders.keys())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise InvalidPublicKeyFileError("No file was found at %s" % path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
publickey = publickey_loader(
|
||||||
|
data=content,
|
||||||
|
backend=backend,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
raise InvalidPublicKeyFileError(e)
|
||||||
|
except UnsupportedAlgorithm as e:
|
||||||
|
raise InvalidAlgorithmError(e)
|
||||||
|
|
||||||
|
return publickey
|
||||||
|
|
||||||
|
|
||||||
|
def compare_publickeys(pk1, pk2):
|
||||||
|
a = isinstance(pk1, Ed25519PublicKey)
|
||||||
|
b = isinstance(pk2, Ed25519PublicKey)
|
||||||
|
if a or b:
|
||||||
|
if not a or not b:
|
||||||
|
return False
|
||||||
|
a = pk1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
||||||
|
b = pk2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
||||||
|
return a == b
|
||||||
|
else:
|
||||||
|
return pk1.public_numbers() == pk2.public_numbers()
|
||||||
|
|
||||||
|
|
||||||
|
def compare_encryption_algorithms(ea1, ea2):
|
||||||
|
if isinstance(ea1, serialization.NoEncryption) and isinstance(ea2, serialization.NoEncryption):
|
||||||
|
return True
|
||||||
|
elif (isinstance(ea1, serialization.BestAvailableEncryption) and
|
||||||
|
isinstance(ea2, serialization.BestAvailableEncryption)):
|
||||||
|
return ea1.password == ea2.password
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_encryption_algorithm(passphrase):
|
||||||
|
try:
|
||||||
|
return serialization.BestAvailableEncryption(passphrase)
|
||||||
|
except ValueError as e:
|
||||||
|
raise InvalidPassphraseError(e)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_comment(comment):
|
||||||
|
if not hasattr(comment, 'encode'):
|
||||||
|
raise InvalidCommentError("%s cannot be encoded to text" % comment)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_comment(path):
|
||||||
|
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise InvalidPublicKeyFileError("No file was found at %s" % path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
fields = f.read().split(b' ', 2)
|
||||||
|
if len(fields) == 3:
|
||||||
|
comment = fields[2].decode(_TEXT_ENCODING)
|
||||||
|
else:
|
||||||
|
comment = ""
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
raise InvalidPublicKeyFileError(e)
|
||||||
|
|
||||||
|
return comment
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_fingerprint(openssh_publickey):
|
||||||
|
digest = hashes.Hash(hashes.SHA256(), backend=backend)
|
||||||
|
decoded_pubkey = b64decode(openssh_publickey.split(b' ')[1])
|
||||||
|
digest.update(decoded_pubkey)
|
||||||
|
|
||||||
|
return 'SHA256:%s' % b64encode(digest.finalize()).decode(encoding=_TEXT_ENCODING).rstrip('=')
|
||||||
403
plugins/module_utils/openssh/utils.py
Normal file
403
plugins/module_utils/openssh/utils.py
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# 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 os
|
||||||
|
import re
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from struct import Struct
|
||||||
|
|
||||||
|
from ansible.module_utils.six import PY3
|
||||||
|
|
||||||
|
# Protocol References
|
||||||
|
# -------------------
|
||||||
|
# https://datatracker.ietf.org/doc/html/rfc4251
|
||||||
|
# https://datatracker.ietf.org/doc/html/rfc4253
|
||||||
|
# https://datatracker.ietf.org/doc/html/rfc5656
|
||||||
|
# https://datatracker.ietf.org/doc/html/rfc8032
|
||||||
|
#
|
||||||
|
# Inspired by:
|
||||||
|
# ------------
|
||||||
|
# https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/serialization/ssh.py
|
||||||
|
# https://github.com/paramiko/paramiko/blob/master/paramiko/message.py
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
long = int
|
||||||
|
|
||||||
|
# 0 (False) or 1 (True) encoded as a single byte
|
||||||
|
_BOOLEAN = Struct(b'?')
|
||||||
|
# Unsigned 8-bit integer in network-byte-order
|
||||||
|
_UBYTE = Struct(b'!B')
|
||||||
|
_UBYTE_MAX = 0xFF
|
||||||
|
# Unsigned 32-bit integer in network-byte-order
|
||||||
|
_UINT32 = Struct(b'!I')
|
||||||
|
# Unsigned 32-bit little endian integer
|
||||||
|
_UINT32_LE = Struct(b'<I')
|
||||||
|
_UINT32_MAX = 0xFFFFFFFF
|
||||||
|
# Unsigned 64-bit integer in network-byte-order
|
||||||
|
_UINT64 = Struct(b'!Q')
|
||||||
|
_UINT64_MAX = 0xFFFFFFFFFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def any_in(sequence, *elements):
|
||||||
|
return any(e in sequence for e in elements)
|
||||||
|
|
||||||
|
|
||||||
|
def file_mode(path):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return 0o000
|
||||||
|
return os.stat(path).st_mode & 0o777
|
||||||
|
|
||||||
|
|
||||||
|
def parse_openssh_version(version_string):
|
||||||
|
"""Parse the version output of ssh -V and return version numbers that can be compared"""
|
||||||
|
|
||||||
|
parsed_result = re.match(
|
||||||
|
r"^.*openssh_(?P<version>[0-9.]+)(p?[0-9]+)[^0-9]*.*$", version_string.lower()
|
||||||
|
)
|
||||||
|
if parsed_result is not None:
|
||||||
|
version = parsed_result.group("version").strip()
|
||||||
|
else:
|
||||||
|
version = None
|
||||||
|
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def secure_open(path, mode):
|
||||||
|
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
|
||||||
|
try:
|
||||||
|
yield fd
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
|
||||||
|
def secure_write(path, mode, content):
|
||||||
|
with secure_open(path, mode) as fd:
|
||||||
|
os.write(fd, content)
|
||||||
|
|
||||||
|
|
||||||
|
# See https://datatracker.ietf.org/doc/html/rfc4251#section-5 for SSH data types
|
||||||
|
class OpensshParser(object):
|
||||||
|
"""Parser for OpenSSH encoded objects"""
|
||||||
|
BOOLEAN_OFFSET = 1
|
||||||
|
UINT32_OFFSET = 4
|
||||||
|
UINT64_OFFSET = 8
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
if not isinstance(data, (bytes, bytearray)):
|
||||||
|
raise TypeError("Data must be bytes-like not %s" % type(data))
|
||||||
|
|
||||||
|
self._data = memoryview(data) if PY3 else data
|
||||||
|
self._pos = 0
|
||||||
|
|
||||||
|
def boolean(self):
|
||||||
|
next_pos = self._check_position(self.BOOLEAN_OFFSET)
|
||||||
|
|
||||||
|
value = _BOOLEAN.unpack(self._data[self._pos:next_pos])[0]
|
||||||
|
self._pos = next_pos
|
||||||
|
return value
|
||||||
|
|
||||||
|
def uint32(self):
|
||||||
|
next_pos = self._check_position(self.UINT32_OFFSET)
|
||||||
|
|
||||||
|
value = _UINT32.unpack(self._data[self._pos:next_pos])[0]
|
||||||
|
self._pos = next_pos
|
||||||
|
return value
|
||||||
|
|
||||||
|
def uint64(self):
|
||||||
|
next_pos = self._check_position(self.UINT64_OFFSET)
|
||||||
|
|
||||||
|
value = _UINT64.unpack(self._data[self._pos:next_pos])[0]
|
||||||
|
self._pos = next_pos
|
||||||
|
return value
|
||||||
|
|
||||||
|
def string(self):
|
||||||
|
length = self.uint32()
|
||||||
|
|
||||||
|
next_pos = self._check_position(length)
|
||||||
|
|
||||||
|
value = self._data[self._pos:next_pos]
|
||||||
|
self._pos = next_pos
|
||||||
|
# Cast to bytes is required as a memoryview slice is itself a memoryview
|
||||||
|
return value if not PY3 else bytes(value)
|
||||||
|
|
||||||
|
def mpint(self):
|
||||||
|
return self._big_int(self.string(), "big", signed=True)
|
||||||
|
|
||||||
|
def name_list(self):
|
||||||
|
raw_string = self.string()
|
||||||
|
return raw_string.decode('ASCII').split(',')
|
||||||
|
|
||||||
|
# Convenience function, but not an official data type from SSH
|
||||||
|
def string_list(self):
|
||||||
|
result = []
|
||||||
|
raw_string = self.string()
|
||||||
|
|
||||||
|
if raw_string:
|
||||||
|
parser = OpensshParser(raw_string)
|
||||||
|
while parser.remaining_bytes():
|
||||||
|
result.append(parser.string())
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Convenience function, but not an official data type from SSH
|
||||||
|
def option_list(self):
|
||||||
|
result = []
|
||||||
|
raw_string = self.string()
|
||||||
|
|
||||||
|
if raw_string:
|
||||||
|
parser = OpensshParser(raw_string)
|
||||||
|
|
||||||
|
while parser.remaining_bytes():
|
||||||
|
name = parser.string()
|
||||||
|
data = parser.string()
|
||||||
|
if data:
|
||||||
|
# data is doubly-encoded
|
||||||
|
data = OpensshParser(data).string()
|
||||||
|
result.append((name, data))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def seek(self, offset):
|
||||||
|
self._pos = self._check_position(offset)
|
||||||
|
|
||||||
|
return self._pos
|
||||||
|
|
||||||
|
def remaining_bytes(self):
|
||||||
|
return len(self._data) - self._pos
|
||||||
|
|
||||||
|
def _check_position(self, offset):
|
||||||
|
if self._pos + offset > len(self._data):
|
||||||
|
raise ValueError("Insufficient data remaining at position: %s" % self._pos)
|
||||||
|
elif self._pos + offset < 0:
|
||||||
|
raise ValueError("Position cannot be less than zero.")
|
||||||
|
else:
|
||||||
|
return self._pos + offset
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def signature_data(cls, signature_string):
|
||||||
|
signature_data = {}
|
||||||
|
|
||||||
|
parser = cls(signature_string)
|
||||||
|
signature_type = parser.string()
|
||||||
|
signature_blob = parser.string()
|
||||||
|
|
||||||
|
blob_parser = cls(signature_blob)
|
||||||
|
if signature_type in (b'ssh-rsa', b'rsa-sha2-256', b'rsa-sha2-512'):
|
||||||
|
# https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||||
|
# https://datatracker.ietf.org/doc/html/rfc8332#section-3
|
||||||
|
signature_data['s'] = cls._big_int(signature_blob, "big")
|
||||||
|
elif signature_type == b'ssh-dss':
|
||||||
|
# https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||||
|
signature_data['r'] = cls._big_int(signature_blob[:20], "big")
|
||||||
|
signature_data['s'] = cls._big_int(signature_blob[20:], "big")
|
||||||
|
elif signature_type in (b'ecdsa-sha2-nistp256', b'ecdsa-sha2-nistp384', b'ecdsa-sha2-nistp521'):
|
||||||
|
# https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2
|
||||||
|
signature_data['r'] = blob_parser.mpint()
|
||||||
|
signature_data['s'] = blob_parser.mpint()
|
||||||
|
elif signature_type == b'ssh-ed25519':
|
||||||
|
# https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2
|
||||||
|
signature_data['R'] = cls._big_int(signature_blob[:32], "little")
|
||||||
|
signature_data['S'] = cls._big_int(signature_blob[32:], "little")
|
||||||
|
else:
|
||||||
|
raise ValueError("%s is not a valid signature type" % signature_type)
|
||||||
|
|
||||||
|
signature_data['signature_type'] = signature_type
|
||||||
|
|
||||||
|
return signature_data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _big_int(cls, raw_string, byte_order, signed=False):
|
||||||
|
if byte_order not in ("big", "little"):
|
||||||
|
raise ValueError("Byte_order must be one of (big, little) not %s" % byte_order)
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
return int.from_bytes(raw_string, byte_order, signed=signed)
|
||||||
|
|
||||||
|
result = 0
|
||||||
|
byte_length = len(raw_string)
|
||||||
|
|
||||||
|
if byte_length > 0:
|
||||||
|
# Check sign-bit
|
||||||
|
msb = raw_string[0] if byte_order == "big" else raw_string[-1]
|
||||||
|
negative = bool(ord(msb) & 0x80)
|
||||||
|
# Match pad value for two's complement
|
||||||
|
pad = b'\xFF' if signed and negative else b'\x00'
|
||||||
|
# The definition of ``mpint`` enforces that unnecessary bytes are not encoded so they are added back
|
||||||
|
pad_length = (4 - byte_length % 4)
|
||||||
|
if pad_length < 4:
|
||||||
|
raw_string = pad * pad_length + raw_string if byte_order == "big" else raw_string + pad * pad_length
|
||||||
|
byte_length += pad_length
|
||||||
|
# Accumulate arbitrary precision integer bytes in the appropriate order
|
||||||
|
if byte_order == "big":
|
||||||
|
for i in range(0, byte_length, cls.UINT32_OFFSET):
|
||||||
|
left_shift = result << cls.UINT32_OFFSET * 8
|
||||||
|
result = left_shift + _UINT32.unpack(raw_string[i:i + cls.UINT32_OFFSET])[0]
|
||||||
|
else:
|
||||||
|
for i in range(byte_length, 0, -cls.UINT32_OFFSET):
|
||||||
|
left_shift = result << cls.UINT32_OFFSET * 8
|
||||||
|
result = left_shift + _UINT32_LE.unpack(raw_string[i - cls.UINT32_OFFSET:i])[0]
|
||||||
|
# Adjust for two's complement
|
||||||
|
if signed and negative:
|
||||||
|
result -= 1 << (8 * byte_length)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class _OpensshWriter(object):
|
||||||
|
"""Writes SSH encoded values to a bytes-like buffer
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
This class is a private API and must not be exported outside of the openssh module_utils.
|
||||||
|
It is not to be used to construct Openssh objects, but rather as a utility to assist
|
||||||
|
in validating parsed material.
|
||||||
|
"""
|
||||||
|
def __init__(self, buffer=None):
|
||||||
|
if buffer is not None:
|
||||||
|
if not isinstance(buffer, (bytes, bytearray)):
|
||||||
|
raise TypeError("Buffer must be a bytes-like object not %s" % type(buffer))
|
||||||
|
else:
|
||||||
|
buffer = bytearray()
|
||||||
|
|
||||||
|
self._buff = buffer
|
||||||
|
|
||||||
|
def boolean(self, value):
|
||||||
|
if not isinstance(value, bool):
|
||||||
|
raise TypeError("Value must be of type bool not %s" % type(value))
|
||||||
|
|
||||||
|
self._buff.extend(_BOOLEAN.pack(value))
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def uint32(self, value):
|
||||||
|
if not isinstance(value, int):
|
||||||
|
raise TypeError("Value must be of type int not %s" % type(value))
|
||||||
|
if value < 0 or value > _UINT32_MAX:
|
||||||
|
raise ValueError("Value must be a positive integer less than %s" % _UINT32_MAX)
|
||||||
|
|
||||||
|
self._buff.extend(_UINT32.pack(value))
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def uint64(self, value):
|
||||||
|
if not isinstance(value, (long, int)):
|
||||||
|
raise TypeError("Value must be of type (long, int) not %s" % type(value))
|
||||||
|
if value < 0 or value > _UINT64_MAX:
|
||||||
|
raise ValueError("Value must be a positive integer less than %s" % _UINT64_MAX)
|
||||||
|
|
||||||
|
self._buff.extend(_UINT64.pack(value))
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def string(self, value):
|
||||||
|
if not isinstance(value, (bytes, bytearray)):
|
||||||
|
raise TypeError("Value must be bytes-like not %s" % type(value))
|
||||||
|
self.uint32(len(value))
|
||||||
|
self._buff.extend(value)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def mpint(self, value):
|
||||||
|
if not isinstance(value, (int, long)):
|
||||||
|
raise TypeError("Value must be of type (long, int) not %s" % type(value))
|
||||||
|
|
||||||
|
self.string(self._int_to_mpint(value))
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def name_list(self, value):
|
||||||
|
if not isinstance(value, list):
|
||||||
|
raise TypeError("Value must be a list of byte strings not %s" % type(value))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.string(','.join(value).encode('ASCII'))
|
||||||
|
except UnicodeEncodeError as e:
|
||||||
|
raise ValueError("Name-list's must consist of US-ASCII characters: %s" % e)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def string_list(self, value):
|
||||||
|
if not isinstance(value, list):
|
||||||
|
raise TypeError("Value must be a list of byte string not %s" % type(value))
|
||||||
|
|
||||||
|
writer = _OpensshWriter()
|
||||||
|
for s in value:
|
||||||
|
writer.string(s)
|
||||||
|
|
||||||
|
self.string(writer.bytes())
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def option_list(self, value):
|
||||||
|
if not isinstance(value, list) or (value and not isinstance(value[0], tuple)):
|
||||||
|
raise TypeError("Value must be a list of tuples")
|
||||||
|
|
||||||
|
writer = _OpensshWriter()
|
||||||
|
for name, data in value:
|
||||||
|
writer.string(name)
|
||||||
|
# SSH option data is encoded twice though this behavior is not documented
|
||||||
|
writer.string(_OpensshWriter().string(data).bytes() if data else bytes())
|
||||||
|
|
||||||
|
self.string(writer.bytes())
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _int_to_mpint(num):
|
||||||
|
if PY3:
|
||||||
|
byte_length = (num.bit_length() + 7) // 8
|
||||||
|
try:
|
||||||
|
result = num.to_bytes(byte_length, "big", signed=True)
|
||||||
|
# Handles values which require \x00 or \xFF to pad sign-bit
|
||||||
|
except OverflowError:
|
||||||
|
result = num.to_bytes(byte_length + 1, "big", signed=True)
|
||||||
|
else:
|
||||||
|
result = bytes()
|
||||||
|
# 0 and -1 are treated as special cases since they are used as sentinels for all other values
|
||||||
|
if num == 0:
|
||||||
|
result += b'\x00'
|
||||||
|
elif num == -1:
|
||||||
|
result += b'\xFF'
|
||||||
|
elif num > 0:
|
||||||
|
while num >> 32:
|
||||||
|
result = _UINT32.pack(num & _UINT32_MAX) + result
|
||||||
|
num = num >> 32
|
||||||
|
# Pack last 4 bytes individually to discard insignificant bytes
|
||||||
|
while num:
|
||||||
|
result = _UBYTE.pack(num & _UBYTE_MAX) + result
|
||||||
|
num = num >> 8
|
||||||
|
# Zero pad final byte if most-significant bit is 1 as per mpint definition
|
||||||
|
if ord(result[0]) & 0x80:
|
||||||
|
result = b'\x00' + result
|
||||||
|
else:
|
||||||
|
while (num >> 32) < -1:
|
||||||
|
result = _UINT32.pack(num & _UINT32_MAX) + result
|
||||||
|
num = num >> 32
|
||||||
|
while num < -1:
|
||||||
|
result = _UBYTE.pack(num & _UBYTE_MAX) + result
|
||||||
|
num = num >> 8
|
||||||
|
if not ord(result[0]) & 0x80:
|
||||||
|
result = b'\xFF' + result
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def bytes(self):
|
||||||
|
return bytes(self._buff)
|
||||||
@@ -739,7 +739,7 @@ class ACMECertificateClient(object):
|
|||||||
raise ModuleFailException('Found no authorization information for "{identifier}"!'.format(
|
raise ModuleFailException('Found no authorization information for "{identifier}"!'.format(
|
||||||
identifier=combine_identifier(identifier_type, identifier)))
|
identifier=combine_identifier(identifier_type, identifier)))
|
||||||
if authz.status != 'valid':
|
if authz.status != 'valid':
|
||||||
authz.raise_error('Status is "{status}" and not "valid"'.format(status=authz.status))
|
authz.raise_error('Status is "{status}" and not "valid"'.format(status=authz.status), module=self.module)
|
||||||
|
|
||||||
if self.version == 1:
|
if self.version == 1:
|
||||||
cert = retrieve_acme_v1_certificate(self.client, pem_to_der(self.csr, self.csr_content))
|
cert = retrieve_acme_v1_certificate(self.client, pem_to_der(self.csr, self.csr_content))
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ def main():
|
|||||||
# but successfully terminate while indicating no change
|
# but successfully terminate while indicating no change
|
||||||
if already_revoked:
|
if already_revoked:
|
||||||
module.exit_json(changed=False)
|
module.exit_json(changed=False)
|
||||||
raise ACMEProtocolException('Failed to revoke certificate', info=info, content_json=result)
|
raise ACMEProtocolException(module, 'Failed to revoke certificate', info=info, content_json=result)
|
||||||
module.exit_json(changed=True)
|
module.exit_json(changed=True)
|
||||||
except ModuleFailException as e:
|
except ModuleFailException as e:
|
||||||
e.do_fail(module)
|
e.do_fail(module)
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ import sys
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
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
|
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
|
||||||
|
|
||||||
@@ -202,7 +202,10 @@ def main():
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
if not HAS_CRYPTOGRAPHY:
|
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:
|
try:
|
||||||
# Get parameters
|
# Get parameters
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ output_json:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
|
||||||
create_backend,
|
create_backend,
|
||||||
@@ -307,7 +307,7 @@ def main():
|
|||||||
pass
|
pass
|
||||||
# Fail if error was returned
|
# Fail if error was returned
|
||||||
if fail_on_acme_error and info['status'] >= 400:
|
if fail_on_acme_error and info['status'] >= 400:
|
||||||
raise ACMEProtocolException(info=info, content_json=result)
|
raise ACMEProtocolException(module, info=info, content=data)
|
||||||
# Done!
|
# Done!
|
||||||
module.exit_json(changed=changed, **result)
|
module.exit_json(changed=changed, **result)
|
||||||
except ModuleFailException as e:
|
except ModuleFailException as e:
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ import os
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||||
split_pem_list,
|
split_pem_list,
|
||||||
|
|||||||
@@ -522,7 +522,7 @@ import traceback
|
|||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||||
write_file,
|
write_file,
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ import datetime
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.ecs.api import (
|
||||||
ecs_client_argument_spec,
|
ecs_client_argument_spec,
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ options:
|
|||||||
- Proxy port used when get a certificate.
|
- Proxy port used when get a certificate.
|
||||||
type: int
|
type: int
|
||||||
default: 8080
|
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:
|
timeout:
|
||||||
description:
|
description:
|
||||||
- The timeout in seconds
|
- The timeout in seconds
|
||||||
@@ -165,7 +173,7 @@ from socket import create_connection, setdefaulttimeout, socket
|
|||||||
from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_REQUIRED
|
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.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 (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||||
cryptography_oid_to_name,
|
cryptography_oid_to_name,
|
||||||
@@ -209,6 +217,20 @@ else:
|
|||||||
CRYPTOGRAPHY_FOUND = True
|
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():
|
def main():
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
argument_spec=dict(
|
argument_spec=dict(
|
||||||
@@ -220,6 +242,7 @@ def main():
|
|||||||
server_name=dict(type='str'),
|
server_name=dict(type='str'),
|
||||||
timeout=dict(type='int', default=10),
|
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', 'pyopenssl', 'cryptography'], default='auto'),
|
||||||
|
starttls=dict(type='str', choices=['mysql']),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -230,6 +253,7 @@ def main():
|
|||||||
proxy_port = module.params.get('proxy_port')
|
proxy_port = module.params.get('proxy_port')
|
||||||
timeout = module.params.get('timeout')
|
timeout = module.params.get('timeout')
|
||||||
server_name = module.params.get('server_name')
|
server_name = module.params.get('server_name')
|
||||||
|
start_tls_server_type = module.params.get('starttls')
|
||||||
|
|
||||||
backend = module.params.get('select_crypto_backend')
|
backend = module.params.get('select_crypto_backend')
|
||||||
if backend == 'auto':
|
if backend == 'auto':
|
||||||
@@ -305,6 +329,9 @@ def main():
|
|||||||
ctx.check_hostname = False
|
ctx.check_hostname = False
|
||||||
ctx.verify_mode = CERT_NONE
|
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 = ctx.wrap_socket(sock, server_hostname=server_name or host).getpeercert(True)
|
||||||
cert = DER_cert_to_PEM_cert(cert)
|
cert = DER_cert_to_PEM_cert(cert)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -356,6 +356,34 @@ LUKS_NAME_REGEX = re.compile(r'\s*crypt\s+([^\s]*)\s*')
|
|||||||
LUKS_DEVICE_REGEX = re.compile(r'\s*device:\s+([^\s]*)\s*')
|
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):
|
class Handler(object):
|
||||||
|
|
||||||
def __init__(self, module):
|
def __init__(self, module):
|
||||||
@@ -515,9 +543,17 @@ class CryptHandler(Handler):
|
|||||||
self.run_luks_close(name)
|
self.run_luks_close(name)
|
||||||
result = self._run_command([wipefs_bin, '--all', device])
|
result = self._run_command([wipefs_bin, '--all', device])
|
||||||
if result[RETURN_CODE] != 0:
|
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]))
|
% (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,
|
def run_luks_add_key(self, device, keyfile, passphrase, new_keyfile,
|
||||||
new_passphrase, pbkdf):
|
new_passphrase, pbkdf):
|
||||||
''' Add new key from a keyfile or passphrase to given 'device';
|
''' Add new key from a keyfile or passphrase to given 'device';
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ requirements:
|
|||||||
options:
|
options:
|
||||||
state:
|
state:
|
||||||
description:
|
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
|
type: str
|
||||||
default: "present"
|
default: "present"
|
||||||
choices: [ 'present', 'absent' ]
|
choices: [ 'present', 'absent' ]
|
||||||
@@ -33,6 +34,7 @@ options:
|
|||||||
force:
|
force:
|
||||||
description:
|
description:
|
||||||
- Should the certificate be regenerated even if it already exists and is valid.
|
- Should the certificate be regenerated even if it already exists and is valid.
|
||||||
|
- Equivalent to I(regenerate=always).
|
||||||
type: bool
|
type: bool
|
||||||
default: false
|
default: false
|
||||||
path:
|
path:
|
||||||
@@ -40,6 +42,46 @@ options:
|
|||||||
- Path of the file containing the certificate.
|
- Path of the file containing the certificate.
|
||||||
type: path
|
type: path
|
||||||
required: true
|
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:
|
signing_key:
|
||||||
description:
|
description:
|
||||||
- The path to the private openssh key that is used for signing the public key in order to generate the certificate.
|
- The path to the private openssh key that is used for signing the public key in order to generate the certificate.
|
||||||
@@ -215,421 +257,292 @@ info:
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import errno
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from datetime import MINYEAR, MAXYEAR
|
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
from shutil import copy2, rmtree
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
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.backends.common import (
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.openssh import parse_openssh_version
|
KeygenCommand,
|
||||||
|
OpensshModule,
|
||||||
|
PrivateKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.openssh.certificate import (
|
||||||
|
OpensshCertificate,
|
||||||
|
OpensshCertificateTimeParameters,
|
||||||
|
parse_option_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CertificateError(Exception):
|
class Certificate(OpensshModule):
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Certificate(object):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
def __init__(self, module):
|
||||||
self.state = module.params['state']
|
super(Certificate, self).__init__(module)
|
||||||
self.force = module.params['force']
|
self.ssh_keygen = KeygenCommand(self.module)
|
||||||
self.type = module.params['type']
|
|
||||||
self.signing_key = module.params['signing_key']
|
self.identifier = self.module.params['identifier'] or ""
|
||||||
self.use_agent = module.params['use_agent']
|
self.options = self.module.params['options'] or []
|
||||||
self.pkcs11_provider = module.params['pkcs11_provider']
|
self.path = self.module.params['path']
|
||||||
self.public_key = module.params['public_key']
|
self.pkcs11_provider = self.module.params['pkcs11_provider']
|
||||||
self.path = module.params['path']
|
self.principals = self.module.params['principals'] or []
|
||||||
self.identifier = module.params['identifier']
|
self.public_key = self.module.params['public_key']
|
||||||
self.serial_number = module.params['serial_number']
|
self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always'
|
||||||
self.valid_from = module.params['valid_from']
|
self.serial_number = self.module.params['serial_number']
|
||||||
self.valid_to = module.params['valid_to']
|
self.signature_algorithm = self.module.params['signature_algorithm']
|
||||||
self.valid_at = module.params['valid_at']
|
self.signing_key = self.module.params['signing_key']
|
||||||
self.principals = module.params['principals']
|
self.state = self.module.params['state']
|
||||||
self.options = module.params['options']
|
self.type = self.module.params['type']
|
||||||
self.changed = False
|
self.use_agent = self.module.params['use_agent']
|
||||||
self.check_mode = module.check_mode
|
self.valid_at = self.module.params['valid_at']
|
||||||
self.cert_info = {}
|
|
||||||
|
self._check_if_base_dir(self.path)
|
||||||
|
|
||||||
if self.state == 'present':
|
if self.state == 'present':
|
||||||
|
self._validate_parameters()
|
||||||
|
|
||||||
if self.options and self.type == "host":
|
self.data = None
|
||||||
module.fail_json(msg="Options can only be used with user certificates.")
|
self.original_data = None
|
||||||
|
if self._exists():
|
||||||
|
self._load_certificate()
|
||||||
|
|
||||||
if self.valid_at:
|
self.time_parameters = None
|
||||||
self.valid_at = self.valid_at.lstrip()
|
if self.state == 'present':
|
||||||
|
self._set_time_parameters()
|
||||||
|
|
||||||
self.valid_from = self.valid_from.lstrip()
|
def _validate_parameters(self):
|
||||||
self.valid_to = self.valid_to.lstrip()
|
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:
|
def _use_agent_available(self):
|
||||||
args = [
|
ssh_version = self._get_ssh_version()
|
||||||
self.ssh_keygen,
|
if not ssh_version:
|
||||||
'-s', self.signing_key
|
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:
|
def _exists(self):
|
||||||
args.extend(['-D', self.pkcs11_provider])
|
return os.path.exists(self.path)
|
||||||
|
|
||||||
if self.use_agent:
|
def _load_certificate(self):
|
||||||
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.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
|
|
||||||
try:
|
try:
|
||||||
return (datetime_one - datetime_two).total_seconds() == 0.0
|
self.original_data = OpensshCertificate.load(self.path)
|
||||||
except AttributeError:
|
except (TypeError, ValueError) as e:
|
||||||
return timedelta_total_seconds(datetime_one - datetime_two) == 0.0
|
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 _set_time_parameters(self):
|
||||||
|
try:
|
||||||
def _check_state():
|
self.time_parameters = OpensshCertificateTimeParameters(
|
||||||
return os.path.exists(self.path)
|
valid_from=self.module.params['valid_from'],
|
||||||
|
valid_to=self.module.params['valid_to'],
|
||||||
if _check_state():
|
)
|
||||||
proc = module.run_command([self.ssh_keygen, '-L', '-f', self.path], environ_update=dict(TZ="UTC"), check_rc=False)
|
except ValueError as e:
|
||||||
if proc[0] != 0:
|
self.module.fail_json(msg=to_native(e))
|
||||||
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 _execute(self):
|
||||||
if self.state == 'present':
|
if self.state == 'present':
|
||||||
result = {
|
if self._should_generate():
|
||||||
'changed': self.changed,
|
self._generate()
|
||||||
'type': self.type,
|
self._update_permissions(self.path)
|
||||||
'filename': self.path,
|
|
||||||
'info': format_cert_info(),
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
result = {
|
if self._exists():
|
||||||
'changed': self.changed,
|
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):
|
def _is_fully_valid(self):
|
||||||
"""Remove the resource from the filesystem."""
|
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:
|
try:
|
||||||
os.remove(self.path)
|
os.remove(self.path)
|
||||||
self.changed = True
|
except OSError as e:
|
||||||
except OSError as exc:
|
self.module.fail_json(msg="Unable to remove existing certificate: %s" % to_native(e))
|
||||||
if exc.errno != errno.ENOENT:
|
|
||||||
raise CertificateError(exc)
|
@property
|
||||||
else:
|
def _result(self):
|
||||||
pass
|
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():
|
def main():
|
||||||
|
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
argument_spec=dict(
|
argument_spec=dict(
|
||||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
|
||||||
force=dict(type='bool', default=False),
|
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'),
|
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'),
|
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_from=dict(type='str'),
|
||||||
valid_to=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,
|
supports_check_mode=True,
|
||||||
add_file_common_args=True,
|
add_file_common_args=True,
|
||||||
required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])],
|
required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])],
|
||||||
)
|
)
|
||||||
|
|
||||||
if module.params['use_agent']:
|
Certificate(module).execute()
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
DOCUMENTATION = '''
|
||||||
---
|
---
|
||||||
module: openssh_keypair
|
module: openssh_keypair
|
||||||
@@ -18,7 +17,9 @@ description:
|
|||||||
ssh-keygen to generate keys. One can generate C(rsa), C(dsa), C(rsa1), C(ed25519)
|
ssh-keygen to generate keys. One can generate C(rsa), C(dsa), C(rsa1), C(ed25519)
|
||||||
or C(ecdsa) private keys."
|
or C(ecdsa) private keys."
|
||||||
requirements:
|
requirements:
|
||||||
- "ssh-keygen"
|
- ssh-keygen (if I(backend=openssh))
|
||||||
|
- cryptography >= 2.6 (if I(backend=cryptography) and OpenSSH < 7.8 is installed)
|
||||||
|
- cryptography >= 3.0 (if I(backend=cryptography) and OpenSSH >= 7.8 is installed)
|
||||||
options:
|
options:
|
||||||
state:
|
state:
|
||||||
description:
|
description:
|
||||||
@@ -55,6 +56,35 @@ options:
|
|||||||
description:
|
description:
|
||||||
- Provides a new comment to the public key.
|
- Provides a new comment to the public key.
|
||||||
type: str
|
type: str
|
||||||
|
passphrase:
|
||||||
|
description:
|
||||||
|
- Passphrase used to decrypt an existing private key or encrypt a newly generated private key.
|
||||||
|
- Passphrases are not supported for I(type=rsa1).
|
||||||
|
- Can only be used when I(backend=cryptography), or when I(backend=auto) and a required C(cryptography) version is installed.
|
||||||
|
type: str
|
||||||
|
version_added: 1.7.0
|
||||||
|
private_key_format:
|
||||||
|
description:
|
||||||
|
- Used when a I(backend=cryptography) to select a format for the private key at the provided I(path).
|
||||||
|
- The only valid option currently is C(auto) which will match the key format of the installed OpenSSH version.
|
||||||
|
- For OpenSSH < 7.8 private keys will be in PKCS1 format except ed25519 keys which will be in OpenSSH format.
|
||||||
|
- For OpenSSH >= 7.8 all private key types will be in the OpenSSH format.
|
||||||
|
type: str
|
||||||
|
default: auto
|
||||||
|
choices:
|
||||||
|
- auto
|
||||||
|
version_added: 1.7.0
|
||||||
|
backend:
|
||||||
|
description:
|
||||||
|
- Selects between the C(cryptography) library or the OpenSSH binary C(opensshbin).
|
||||||
|
- C(auto) will default to C(opensshbin) unless the OpenSSH binary is not installed or when using I(passphrase).
|
||||||
|
type: str
|
||||||
|
default: auto
|
||||||
|
choices:
|
||||||
|
- auto
|
||||||
|
- cryptography
|
||||||
|
- opensshbin
|
||||||
|
version_added: 1.7.0
|
||||||
regenerate:
|
regenerate:
|
||||||
description:
|
description:
|
||||||
- Allows to configure in which situations the module is allowed to regenerate private keys.
|
- Allows to configure in which situations the module is allowed to regenerate private keys.
|
||||||
@@ -92,6 +122,7 @@ notes:
|
|||||||
- In case the ssh key is broken or password protected, the module will fail.
|
- 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.
|
Set the I(force) option to C(yes) if you want to regenerate the keypair.
|
||||||
- Supports C(check_mode).
|
- 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
|
extends_documentation_fragment: files
|
||||||
'''
|
'''
|
||||||
@@ -101,6 +132,11 @@ EXAMPLES = '''
|
|||||||
community.crypto.openssh_keypair:
|
community.crypto.openssh_keypair:
|
||||||
path: /tmp/id_ssh_rsa
|
path: /tmp/id_ssh_rsa
|
||||||
|
|
||||||
|
- name: Generate an OpenSSH keypair with the default values (4096 bits, rsa) and encrypted private key
|
||||||
|
community.crypto.openssh_keypair:
|
||||||
|
path: /tmp/id_ssh_rsa
|
||||||
|
passphrase: super_secret_password
|
||||||
|
|
||||||
- name: Generate an OpenSSH rsa keypair with a different size (2048 bits)
|
- name: Generate an OpenSSH rsa keypair with a different size (2048 bits)
|
||||||
community.crypto.openssh_keypair:
|
community.crypto.openssh_keypair:
|
||||||
path: /tmp/id_ssh_rsa
|
path: /tmp/id_ssh_rsa
|
||||||
@@ -150,281 +186,15 @@ comment:
|
|||||||
sample: test@comment
|
sample: test@comment
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import errno
|
|
||||||
import os
|
|
||||||
import stat
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
from ansible.module_utils._text import to_native
|
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.keypair_backend import (
|
||||||
class KeypairError(Exception):
|
select_backend
|
||||||
pass
|
)
|
||||||
|
|
||||||
|
|
||||||
class Keypair(object):
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
self.path = module.params['path']
|
|
||||||
self.state = module.params['state']
|
|
||||||
self.force = module.params['force']
|
|
||||||
self.size = module.params['size']
|
|
||||||
self.type = module.params['type']
|
|
||||||
self.comment = module.params['comment']
|
|
||||||
self.changed = False
|
|
||||||
self.check_mode = module.check_mode
|
|
||||||
self.privatekey = None
|
|
||||||
self.fingerprint = {}
|
|
||||||
self.public_key = {}
|
|
||||||
self.regenerate = module.params['regenerate']
|
|
||||||
if self.regenerate == 'always':
|
|
||||||
self.force = True
|
|
||||||
|
|
||||||
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.'))
|
|
||||||
|
|
||||||
if 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.'))
|
|
||||||
|
|
||||||
if 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. '))
|
|
||||||
if self.type == 'ed25519':
|
|
||||||
self.size = 256
|
|
||||||
|
|
||||||
def generate(self, module):
|
|
||||||
# generate a keypair
|
|
||||||
if self.force or not self.isPrivateKeyValid(module, perms_required=False):
|
|
||||||
args = [
|
|
||||||
module.get_bin_path('ssh-keygen', True),
|
|
||||||
'-q',
|
|
||||||
'-N', '',
|
|
||||||
'-b', str(self.size),
|
|
||||||
'-t', self.type,
|
|
||||||
'-f', self.path,
|
|
||||||
]
|
|
||||||
|
|
||||||
if self.comment:
|
|
||||||
args.extend(['-C', self.comment])
|
|
||||||
else:
|
|
||||||
args.extend(['-C', ""])
|
|
||||||
|
|
||||||
try:
|
|
||||||
if os.path.exists(self.path) and not os.access(self.path, os.W_OK):
|
|
||||||
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
|
|
||||||
self.changed = True
|
|
||||||
stdin_data = None
|
|
||||||
if os.path.exists(self.path):
|
|
||||||
stdin_data = 'y'
|
|
||||||
module.run_command(args, data=stdin_data)
|
|
||||||
proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path])
|
|
||||||
self.fingerprint = proc[1].split()
|
|
||||||
pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path])
|
|
||||||
self.public_key = pubkey[1].strip('\n')
|
|
||||||
except Exception as e:
|
|
||||||
self.remove()
|
|
||||||
module.fail_json(msg="%s" % to_native(e))
|
|
||||||
|
|
||||||
elif not self.isPublicKeyValid(module, perms_required=False):
|
|
||||||
pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path])
|
|
||||||
pubkey = pubkey[1].strip('\n')
|
|
||||||
try:
|
|
||||||
self.changed = True
|
|
||||||
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:
|
|
||||||
module.fail_json(
|
|
||||||
msg='The public key is missing or does not match the private key. '
|
|
||||||
'Unable to regenerate the public key.')
|
|
||||||
self.public_key = pubkey
|
|
||||||
|
|
||||||
if self.comment:
|
|
||||||
try:
|
|
||||||
if os.path.exists(self.path) and not os.access(self.path, os.W_OK):
|
|
||||||
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
|
|
||||||
args = [module.get_bin_path('ssh-keygen', True),
|
|
||||||
'-q', '-o', '-c', '-C', self.comment, '-f', self.path]
|
|
||||||
module.run_command(args)
|
|
||||||
except IOError:
|
|
||||||
module.fail_json(
|
|
||||||
msg='Unable to update the comment for the public key.')
|
|
||||||
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
|
||||||
if module.set_fs_attributes_if_different(file_args, False):
|
|
||||||
self.changed = True
|
|
||||||
file_args['path'] = file_args['path'] + '.pub'
|
|
||||||
if module.set_fs_attributes_if_different(file_args, False):
|
|
||||||
self.changed = True
|
|
||||||
|
|
||||||
def _check_pass_protected_or_broken_key(self, module):
|
|
||||||
key_state = module.run_command([module.get_bin_path('ssh-keygen', True),
|
|
||||||
'-P', '', '-yf', self.path], check_rc=False)
|
|
||||||
if key_state[0] == 255 or 'is not a public key file' in key_state[2]:
|
|
||||||
return True
|
|
||||||
if 'incorrect passphrase' in key_state[2] or 'load failed' in key_state[2]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def isPrivateKeyValid(self, module, perms_required=True):
|
|
||||||
|
|
||||||
# check if the key is correct
|
|
||||||
def _check_state():
|
|
||||||
return os.path.exists(self.path)
|
|
||||||
|
|
||||||
if not _check_state():
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self._check_pass_protected_or_broken_key(module):
|
|
||||||
if self.regenerate in ('full_idempotence', 'always'):
|
|
||||||
return False
|
|
||||||
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`.')
|
|
||||||
|
|
||||||
proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path], check_rc=False)
|
|
||||||
if not proc[0] == 0:
|
|
||||||
if os.path.isdir(self.path):
|
|
||||||
module.fail_json(msg='%s is a directory. Please specify a path to a file.' % (self.path))
|
|
||||||
|
|
||||||
if self.regenerate in ('full_idempotence', 'always'):
|
|
||||||
return False
|
|
||||||
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`.')
|
|
||||||
|
|
||||||
fingerprint = proc[1].split()
|
|
||||||
keysize = int(fingerprint[0])
|
|
||||||
keytype = fingerprint[-1][1:-1].lower()
|
|
||||||
|
|
||||||
self.fingerprint = fingerprint
|
|
||||||
|
|
||||||
if self.regenerate == 'never':
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _check_type():
|
|
||||||
return self.type == keytype
|
|
||||||
|
|
||||||
def _check_size():
|
|
||||||
return self.size == keysize
|
|
||||||
|
|
||||||
if not (_check_type() and _check_size()):
|
|
||||||
if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'):
|
|
||||||
return False
|
|
||||||
module.fail_json(msg='Key has wrong type and/or size.'
|
|
||||||
' Will not proceed. To force regeneration, call the module with `generate`'
|
|
||||||
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.')
|
|
||||||
|
|
||||||
def _check_perms(module):
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
|
||||||
return not module.set_fs_attributes_if_different(file_args, False)
|
|
||||||
|
|
||||||
return not perms_required or _check_perms(module)
|
|
||||||
|
|
||||||
def isPublicKeyValid(self, module, perms_required=True):
|
|
||||||
|
|
||||||
def _get_pubkey_content():
|
|
||||||
if os.path.exists(self.path + ".pub"):
|
|
||||||
with open(self.path + ".pub", "r") as pubkey_f:
|
|
||||||
present_pubkey = pubkey_f.read().strip(' \n')
|
|
||||||
return present_pubkey
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _parse_pubkey(pubkey_content):
|
|
||||||
if pubkey_content:
|
|
||||||
parts = pubkey_content.split(' ', 2)
|
|
||||||
if len(parts) < 2:
|
|
||||||
return False
|
|
||||||
return parts[0], parts[1], '' if len(parts) <= 2 else parts[2]
|
|
||||||
return False
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def _check_perms(module):
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
|
||||||
file_args['path'] = file_args['path'] + '.pub'
|
|
||||||
return not module.set_fs_attributes_if_different(file_args, False)
|
|
||||||
|
|
||||||
pubkey_parts = _parse_pubkey(_get_pubkey_content())
|
|
||||||
|
|
||||||
pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path])
|
|
||||||
pubkey = pubkey[1].strip('\n')
|
|
||||||
if _pubkey_valid(pubkey):
|
|
||||||
self.public_key = pubkey
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.comment:
|
|
||||||
if not _comment_valid():
|
|
||||||
return False
|
|
||||||
|
|
||||||
if perms_required:
|
|
||||||
if not _check_perms(module):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
# return result as a dict
|
|
||||||
|
|
||||||
"""Serialize the object into a dictionary."""
|
|
||||||
result = {
|
|
||||||
'changed': self.changed,
|
|
||||||
'size': self.size,
|
|
||||||
'type': self.type,
|
|
||||||
'filename': self.path,
|
|
||||||
# On removal this has no value
|
|
||||||
'fingerprint': self.fingerprint[1] if self.fingerprint else '',
|
|
||||||
'public_key': self.public_key,
|
|
||||||
'comment': self.comment if self.comment else '',
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def remove(self):
|
|
||||||
"""Remove the resource from the filesystem."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.remove(self.path)
|
|
||||||
self.changed = True
|
|
||||||
except OSError as exc:
|
|
||||||
if exc.errno != errno.ENOENT:
|
|
||||||
raise KeypairError(exc)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if os.path.exists(self.path + ".pub"):
|
|
||||||
try:
|
|
||||||
os.remove(self.path + ".pub")
|
|
||||||
self.changed = True
|
|
||||||
except OSError as exc:
|
|
||||||
if exc.errno != errno.ENOENT:
|
|
||||||
raise KeypairError(exc)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
# Define Ansible Module
|
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
argument_spec=dict(
|
argument_spec=dict(
|
||||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||||
@@ -438,49 +208,17 @@ def main():
|
|||||||
default='partial_idempotence',
|
default='partial_idempotence',
|
||||||
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
|
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
|
||||||
),
|
),
|
||||||
|
passphrase=dict(type='str', no_log=True),
|
||||||
|
private_key_format=dict(type='str', default='auto', no_log=False, choices=['auto']),
|
||||||
|
backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'opensshbin'])
|
||||||
),
|
),
|
||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
add_file_common_args=True,
|
add_file_common_args=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if Path exists
|
keypair = select_backend(module, module.params['backend'])[1]
|
||||||
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 = Keypair(module)
|
keypair.execute()
|
||||||
|
|
||||||
if keypair.state == 'present':
|
|
||||||
|
|
||||||
if module.check_mode:
|
|
||||||
result = keypair.dump()
|
|
||||||
result['changed'] = keypair.force or not keypair.isPrivateKeyValid(module) or not keypair.isPublicKeyValid(module)
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
try:
|
|
||||||
keypair.generate(module)
|
|
||||||
except Exception as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
else:
|
|
||||||
|
|
||||||
if module.check_mode:
|
|
||||||
keypair.changed = os.path.exists(module.params['path'])
|
|
||||||
if keypair.changed:
|
|
||||||
keypair.fingerprint = {}
|
|
||||||
result = keypair.dump()
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
try:
|
|
||||||
keypair.remove()
|
|
||||||
except Exception as exc:
|
|
||||||
module.fail_json(msg=to_native(exc))
|
|
||||||
|
|
||||||
result = keypair.dump()
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ csr:
|
|||||||
|
|
||||||
import os
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr import (
|
||||||
select_backend,
|
select_backend,
|
||||||
@@ -286,7 +286,10 @@ class CertificateSigningRequestModule(OpenSSLObject):
|
|||||||
self.changed = True
|
self.changed = True
|
||||||
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
file_args = module.load_file_common_arguments(module.params)
|
||||||
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
|
if module.check_file_absent_if_check_mode(file_args['path']):
|
||||||
|
self.changed = True
|
||||||
|
else:
|
||||||
|
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
|
||||||
|
|
||||||
def remove(self, module):
|
def remove(self, module):
|
||||||
self.module_backend.set_existing(None)
|
self.module_backend.set_existing(None)
|
||||||
|
|||||||
@@ -183,6 +183,77 @@ public_key:
|
|||||||
returned: success
|
returned: success
|
||||||
type: str
|
type: str
|
||||||
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
||||||
|
public_key_type:
|
||||||
|
description:
|
||||||
|
- The CSR's public key's type.
|
||||||
|
- One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
|
||||||
|
- Will start with C(unknown) if the key type cannot be determined.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
version_added: 1.7.0
|
||||||
|
sample: RSA
|
||||||
|
public_key_data:
|
||||||
|
description:
|
||||||
|
- Public key data. Depends on the public key's type.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
version_added: 1.7.0
|
||||||
|
contains:
|
||||||
|
size:
|
||||||
|
description:
|
||||||
|
- Bit size of modulus (RSA) or prime number (DSA).
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=RSA) or C(public_key_type=DSA)
|
||||||
|
modulus:
|
||||||
|
description:
|
||||||
|
- The RSA key's modulus.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=RSA)
|
||||||
|
exponent:
|
||||||
|
description:
|
||||||
|
- The RSA key's public exponent.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=RSA)
|
||||||
|
p:
|
||||||
|
description:
|
||||||
|
- The C(p) value for DSA.
|
||||||
|
- This is the prime modulus upon which arithmetic takes place.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=DSA)
|
||||||
|
q:
|
||||||
|
description:
|
||||||
|
- The C(q) value for DSA.
|
||||||
|
- This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
|
||||||
|
multiplicative group of the prime field used.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=DSA)
|
||||||
|
g:
|
||||||
|
description:
|
||||||
|
- The C(g) value for DSA.
|
||||||
|
- This is the element spanning the subgroup of the multiplicative group of the prime field used.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=DSA)
|
||||||
|
curve:
|
||||||
|
description:
|
||||||
|
- The curve's name for ECC.
|
||||||
|
type: str
|
||||||
|
returned: When C(public_key_type=ECC)
|
||||||
|
exponent_size:
|
||||||
|
description:
|
||||||
|
- The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=ECC)
|
||||||
|
x:
|
||||||
|
description:
|
||||||
|
- The C(x) coordinate for the public point on the elliptic curve.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=ECC)
|
||||||
|
y:
|
||||||
|
description:
|
||||||
|
- For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
|
||||||
|
- For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=DSA) or C(public_key_type=ECC)
|
||||||
public_key_fingerprints:
|
public_key_fingerprints:
|
||||||
description:
|
description:
|
||||||
- Fingerprints of CSR's public key.
|
- Fingerprints of CSR's public key.
|
||||||
@@ -225,428 +296,17 @@ authority_cert_serial_number:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
import abc
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
import binascii
|
from ansible.module_utils.common.text.converters import to_native
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
OpenSSLObjectError,
|
OpenSSLObjectError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import (
|
||||||
OpenSSLObject,
|
select_backend,
|
||||||
load_certificate_request,
|
|
||||||
get_fingerprint_of_bytes,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
|
||||||
cryptography_decode_name,
|
|
||||||
cryptography_get_extensions_from_csr,
|
|
||||||
cryptography_oid_to_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
|
|
||||||
pyopenssl_get_extensions_from_csr,
|
|
||||||
pyopenssl_normalize_name,
|
|
||||||
pyopenssl_normalize_name_attribute,
|
|
||||||
pyopenssl_parse_name_constraints,
|
|
||||||
)
|
|
||||||
|
|
||||||
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:
|
|
||||||
import cryptography
|
|
||||||
from cryptography import x509
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
|
|
||||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateSigningRequestInfo(OpenSSLObject):
|
|
||||||
def __init__(self, module, backend):
|
|
||||||
super(CertificateSigningRequestInfo, self).__init__(
|
|
||||||
module.params['path'] or '',
|
|
||||||
'present',
|
|
||||||
False,
|
|
||||||
module.check_mode,
|
|
||||||
)
|
|
||||||
self.backend = backend
|
|
||||||
self.module = module
|
|
||||||
self.content = module.params['content']
|
|
||||||
if self.content is not None:
|
|
||||||
self.content = self.content.encode('utf-8')
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
# Empty method because OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
# Empty method because OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_subject_ordered(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_key_usage(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_extended_key_usage(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_basic_constraints(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_ocsp_must_staple(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_subject_alt_name(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_name_constraints(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_subject_key_identifier(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_authority_key_identifier(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_all_extensions(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _is_signature_valid(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_info(self):
|
|
||||||
result = dict()
|
|
||||||
self.csr = load_certificate_request(self.path, content=self.content, backend=self.backend)
|
|
||||||
|
|
||||||
subject = self._get_subject_ordered()
|
|
||||||
result['subject'] = dict()
|
|
||||||
for k, v in subject:
|
|
||||||
result['subject'][k] = v
|
|
||||||
result['subject_ordered'] = subject
|
|
||||||
result['key_usage'], result['key_usage_critical'] = self._get_key_usage()
|
|
||||||
result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage()
|
|
||||||
result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints()
|
|
||||||
result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple()
|
|
||||||
result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name()
|
|
||||||
(
|
|
||||||
result['name_constraints_permitted'],
|
|
||||||
result['name_constraints_excluded'],
|
|
||||||
result['name_constraints_critical'],
|
|
||||||
) = self._get_name_constraints()
|
|
||||||
|
|
||||||
result['public_key'] = self._get_public_key(binary=False)
|
|
||||||
pk = self._get_public_key(binary=True)
|
|
||||||
result['public_key_fingerprints'] = get_fingerprint_of_bytes(pk) if pk is not None else dict()
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
aki, aci, acsn = self._get_authority_key_identifier()
|
|
||||||
if aki is not None:
|
|
||||||
aki = to_native(binascii.hexlify(aki))
|
|
||||||
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
|
||||||
result['authority_key_identifier'] = aki
|
|
||||||
result['authority_cert_issuer'] = aci
|
|
||||||
result['authority_cert_serial_number'] = acsn
|
|
||||||
|
|
||||||
result['extensions_by_oid'] = self._get_all_extensions()
|
|
||||||
|
|
||||||
result['signature_valid'] = self._is_signature_valid()
|
|
||||||
if not result['signature_valid']:
|
|
||||||
self.module.fail_json(
|
|
||||||
msg='CSR signature is invalid!',
|
|
||||||
**result
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateSigningRequestInfoCryptography(CertificateSigningRequestInfo):
|
|
||||||
"""Validate the supplied CSR, using the cryptography backend"""
|
|
||||||
def __init__(self, module):
|
|
||||||
super(CertificateSigningRequestInfoCryptography, self).__init__(module, 'cryptography')
|
|
||||||
|
|
||||||
def _get_subject_ordered(self):
|
|
||||||
result = []
|
|
||||||
for attribute in self.csr.subject:
|
|
||||||
result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _get_key_usage(self):
|
|
||||||
try:
|
|
||||||
current_key_ext = self.csr.extensions.get_extension_for_class(x509.KeyUsage)
|
|
||||||
current_key_usage = current_key_ext.value
|
|
||||||
key_usage = dict(
|
|
||||||
digital_signature=current_key_usage.digital_signature,
|
|
||||||
content_commitment=current_key_usage.content_commitment,
|
|
||||||
key_encipherment=current_key_usage.key_encipherment,
|
|
||||||
data_encipherment=current_key_usage.data_encipherment,
|
|
||||||
key_agreement=current_key_usage.key_agreement,
|
|
||||||
key_cert_sign=current_key_usage.key_cert_sign,
|
|
||||||
crl_sign=current_key_usage.crl_sign,
|
|
||||||
encipher_only=False,
|
|
||||||
decipher_only=False,
|
|
||||||
)
|
|
||||||
if key_usage['key_agreement']:
|
|
||||||
key_usage.update(dict(
|
|
||||||
encipher_only=current_key_usage.encipher_only,
|
|
||||||
decipher_only=current_key_usage.decipher_only
|
|
||||||
))
|
|
||||||
|
|
||||||
key_usage_names = dict(
|
|
||||||
digital_signature='Digital Signature',
|
|
||||||
content_commitment='Non Repudiation',
|
|
||||||
key_encipherment='Key Encipherment',
|
|
||||||
data_encipherment='Data Encipherment',
|
|
||||||
key_agreement='Key Agreement',
|
|
||||||
key_cert_sign='Certificate Sign',
|
|
||||||
crl_sign='CRL Sign',
|
|
||||||
encipher_only='Encipher Only',
|
|
||||||
decipher_only='Decipher Only',
|
|
||||||
)
|
|
||||||
return sorted([
|
|
||||||
key_usage_names[name] for name, value in key_usage.items() if value
|
|
||||||
]), current_key_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_extended_key_usage(self):
|
|
||||||
try:
|
|
||||||
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
|
|
||||||
return sorted([
|
|
||||||
cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value
|
|
||||||
]), ext_keyusage_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_basic_constraints(self):
|
|
||||||
try:
|
|
||||||
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.BasicConstraints)
|
|
||||||
result = []
|
|
||||||
result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE'))
|
|
||||||
if ext_keyusage_ext.value.path_length is not None:
|
|
||||||
result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length))
|
|
||||||
return sorted(result), ext_keyusage_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_ocsp_must_staple(self):
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
# This only works with cryptography >= 2.1
|
|
||||||
tlsfeature_ext = self.csr.extensions.get_extension_for_class(x509.TLSFeature)
|
|
||||||
value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
|
|
||||||
except AttributeError as dummy:
|
|
||||||
# Fallback for cryptography < 2.1
|
|
||||||
oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
|
|
||||||
tlsfeature_ext = self.csr.extensions.get_extension_for_oid(oid)
|
|
||||||
value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05"
|
|
||||||
return value, tlsfeature_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_subject_alt_name(self):
|
|
||||||
try:
|
|
||||||
san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
|
||||||
result = [cryptography_decode_name(san) for san in san_ext.value]
|
|
||||||
return result, san_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_name_constraints(self):
|
|
||||||
try:
|
|
||||||
nc_ext = self.csr.extensions.get_extension_for_class(x509.NameConstraints)
|
|
||||||
permitted = [cryptography_decode_name(san) for san in nc_ext.value.permitted_subtrees or []]
|
|
||||||
excluded = [cryptography_decode_name(san) for san in nc_ext.value.excluded_subtrees or []]
|
|
||||||
return permitted, excluded, nc_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, None, False
|
|
||||||
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
return self.csr.public_key().public_bytes(
|
|
||||||
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
|
||||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_subject_key_identifier(self):
|
|
||||||
try:
|
|
||||||
ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
|
||||||
return ext.value.digest
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_authority_key_identifier(self):
|
|
||||||
try:
|
|
||||||
ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
|
||||||
issuer = None
|
|
||||||
if ext.value.authority_cert_issuer is not None:
|
|
||||||
issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
|
|
||||||
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
def _get_all_extensions(self):
|
|
||||||
return cryptography_get_extensions_from_csr(self.csr)
|
|
||||||
|
|
||||||
def _is_signature_valid(self):
|
|
||||||
return self.csr.is_signature_valid
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateSigningRequestInfoPyOpenSSL(CertificateSigningRequestInfo):
|
|
||||||
"""validate the supplied CSR."""
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(CertificateSigningRequestInfoPyOpenSSL, self).__init__(module, 'pyopenssl')
|
|
||||||
|
|
||||||
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(self, binary):
|
|
||||||
try:
|
|
||||||
return crypto.dump_publickey(
|
|
||||||
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
|
|
||||||
self.csr.get_pubkey()
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
bio = crypto._new_mem_buf()
|
|
||||||
if binary:
|
|
||||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.csr.get_pubkey()._pkey)
|
|
||||||
else:
|
|
||||||
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_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 main():
|
def main():
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
@@ -664,53 +324,19 @@ def main():
|
|||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if module.params['content'] is not None:
|
||||||
|
data = module.params['content'].encode('utf-8')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with open(module.params['path'], 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
module.fail_json(msg='Error while reading CSR file from disk: {0}'.format(e))
|
||||||
|
|
||||||
|
backend, module_backend = select_backend(module, module.params['select_crypto_backend'], data, validate_signature=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if module.params['path'] is not None:
|
result = module_backend.get_info()
|
||||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
|
||||||
if not os.path.isdir(base_dir):
|
|
||||||
module.fail_json(
|
|
||||||
name=base_dir,
|
|
||||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
backend = module.params['select_crypto_backend']
|
|
||||||
if backend == 'auto':
|
|
||||||
# Detect what backend we can use
|
|
||||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
|
||||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
|
||||||
|
|
||||||
# If cryptography is available we'll use it
|
|
||||||
if can_use_cryptography:
|
|
||||||
backend = 'cryptography'
|
|
||||||
elif can_use_pyopenssl:
|
|
||||||
backend = 'pyopenssl'
|
|
||||||
|
|
||||||
# Fail if no backend has been found
|
|
||||||
if backend == 'auto':
|
|
||||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
|
||||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
|
||||||
MINIMAL_PYOPENSSL_VERSION))
|
|
||||||
|
|
||||||
if backend == 'pyopenssl':
|
|
||||||
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')
|
|
||||||
|
|
||||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
|
||||||
version='2.0.0', collection_name='community.crypto')
|
|
||||||
certificate = CertificateSigningRequestInfoPyOpenSSL(module)
|
|
||||||
elif backend == 'cryptography':
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
certificate = CertificateSigningRequestInfoCryptography(module)
|
|
||||||
|
|
||||||
result = certificate.get_info()
|
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
except OpenSSLObjectError as exc:
|
except OpenSSLObjectError as exc:
|
||||||
module.fail_json(msg=to_native(exc))
|
module.fail_json(msg=to_native(exc))
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ csr:
|
|||||||
type: str
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr import (
|
||||||
select_backend,
|
select_backend,
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ import traceback
|
|||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||||
load_file_if_exists,
|
load_file_if_exists,
|
||||||
@@ -221,9 +221,9 @@ class DHParameterBase(object):
|
|||||||
def _check_fs_attributes(self, module):
|
def _check_fs_attributes(self, module):
|
||||||
"""Checks (and changes if not in check mode!) fs attributes"""
|
"""Checks (and changes if not in check mode!) fs attributes"""
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
file_args = module.load_file_common_arguments(module.params)
|
||||||
attrs_changed = module.set_fs_attributes_if_different(file_args, False)
|
if module.check_file_absent_if_check_mode(file_args['path']):
|
||||||
|
return False
|
||||||
return not attrs_changed
|
return not module.set_fs_attributes_if_different(file_args, False)
|
||||||
|
|
||||||
def dump(self):
|
def dump(self):
|
||||||
"""Serialize the object into a dictionary."""
|
"""Serialize the object into a dictionary."""
|
||||||
|
|||||||
@@ -16,8 +16,14 @@ author:
|
|||||||
short_description: Generate OpenSSL PKCS#12 archive
|
short_description: Generate OpenSSL PKCS#12 archive
|
||||||
description:
|
description:
|
||||||
- This module allows one to (re-)generate PKCS#12.
|
- This module allows one to (re-)generate PKCS#12.
|
||||||
|
- The module can use the cryptography Python library, or the pyOpenSSL Python
|
||||||
|
library. By default, it tries to detect which one is available, assuming none of the
|
||||||
|
I(iter_size) and I(maciter_size) options are used. This can be overridden with the
|
||||||
|
I(select_crypto_backend) option.
|
||||||
|
# Please note that the C(pyopenssl) backend has been deprecated in community.crypto x.y.0,
|
||||||
|
# and will be removed in community.crypto (x+1).0.0.
|
||||||
requirements:
|
requirements:
|
||||||
- python-pyOpenSSL
|
- PyOpenSSL >= 0.15 or cryptography >= 3.0
|
||||||
options:
|
options:
|
||||||
action:
|
action:
|
||||||
description:
|
description:
|
||||||
@@ -58,16 +64,21 @@ options:
|
|||||||
iter_size:
|
iter_size:
|
||||||
description:
|
description:
|
||||||
- Number of times to repeat the encryption step.
|
- Number of times to repeat the encryption step.
|
||||||
|
- This is not considered during idempotency checks.
|
||||||
|
- This is only used by the C(pyopenssl) backend. When using it, the default is C(2048).
|
||||||
type: int
|
type: int
|
||||||
default: 2048
|
|
||||||
maciter_size:
|
maciter_size:
|
||||||
description:
|
description:
|
||||||
- Number of times to repeat the MAC step.
|
- Number of times to repeat the MAC step.
|
||||||
|
- This is not considered during idempotency checks.
|
||||||
|
- This is only used by the C(pyopenssl) backend. When using it, the default is C(1).
|
||||||
type: int
|
type: int
|
||||||
default: 1
|
|
||||||
passphrase:
|
passphrase:
|
||||||
description:
|
description:
|
||||||
- The PKCS#12 password.
|
- The PKCS#12 password.
|
||||||
|
- "B(Note:) PKCS12 encryption is not secure and should not be used as a security mechanism.
|
||||||
|
If you need to store or send a PKCS12 file safely, you should additionally encrypt it
|
||||||
|
with something else."
|
||||||
type: str
|
type: str
|
||||||
path:
|
path:
|
||||||
description:
|
description:
|
||||||
@@ -105,6 +116,21 @@ options:
|
|||||||
type: bool
|
type: bool
|
||||||
default: no
|
default: no
|
||||||
version_added: "1.0.0"
|
version_added: "1.0.0"
|
||||||
|
select_crypto_backend:
|
||||||
|
description:
|
||||||
|
- Determines which crypto backend to use.
|
||||||
|
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||||
|
If one of I(iter_size) or I(maciter_size) is used, C(auto) will always result in C(pyopenssl) to be chosen
|
||||||
|
for backwards compatibility.
|
||||||
|
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||||
|
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||||
|
# - Please note that the C(pyopenssl) backend has been deprecated in community.crypto x.y.0, and will be
|
||||||
|
# removed in community.crypto (x+1).0.0.
|
||||||
|
# From that point on, only the C(cryptography) backend will be available.
|
||||||
|
type: str
|
||||||
|
default: auto
|
||||||
|
choices: [ auto, cryptography, pyopenssl ]
|
||||||
|
version_added: 1.7.0
|
||||||
extends_documentation_fragment:
|
extends_documentation_fragment:
|
||||||
- files
|
- files
|
||||||
seealso:
|
seealso:
|
||||||
@@ -207,13 +233,16 @@ pkcs12:
|
|||||||
version_added: "1.0.0"
|
version_added: "1.0.0"
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
import abc
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||||
load_file_if_exists,
|
load_file_if_exists,
|
||||||
@@ -225,6 +254,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.basic impo
|
|||||||
OpenSSLBadPassphraseError,
|
OpenSSLBadPassphraseError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||||
|
parse_pkcs12,
|
||||||
|
)
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
OpenSSLObject,
|
OpenSSLObject,
|
||||||
load_privatekey,
|
load_privatekey,
|
||||||
@@ -235,23 +268,40 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import
|
|||||||
split_pem_list,
|
split_pem_list,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MINIMAL_CRYPTOGRAPHY_VERSION = '3.0'
|
||||||
|
MINIMAL_PYOPENSSL_VERSION = '0.15'
|
||||||
|
|
||||||
PYOPENSSL_IMP_ERR = None
|
PYOPENSSL_IMP_ERR = None
|
||||||
try:
|
try:
|
||||||
|
import OpenSSL
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
|
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||||
pyopenssl_found = False
|
PYOPENSSL_FOUND = False
|
||||||
else:
|
else:
|
||||||
pyopenssl_found = True
|
PYOPENSSL_FOUND = True
|
||||||
|
|
||||||
|
CRYPTOGRAPHY_IMP_ERR = None
|
||||||
|
try:
|
||||||
|
import cryptography
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates
|
||||||
|
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||||
|
except ImportError:
|
||||||
|
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||||
|
CRYPTOGRAPHY_FOUND = False
|
||||||
|
else:
|
||||||
|
CRYPTOGRAPHY_FOUND = True
|
||||||
|
|
||||||
|
|
||||||
def load_certificate_set(filename):
|
def load_certificate_set(filename, backend):
|
||||||
'''
|
'''
|
||||||
Load list of concatenated PEM files, and return a list of parsed certificates.
|
Load list of concatenated PEM files, and return a list of parsed certificates.
|
||||||
'''
|
'''
|
||||||
with open(filename, 'rb') as f:
|
with open(filename, 'rb') as f:
|
||||||
data = f.read().decode('utf-8')
|
data = f.read().decode('utf-8')
|
||||||
return [load_certificate(None, content=cert) for cert in split_pem_list(data)]
|
return [load_certificate(None, content=cert.encode('utf-8'), backend=backend) for cert in split_pem_list(data)]
|
||||||
|
|
||||||
|
|
||||||
class PkcsError(OpenSSLObjectError):
|
class PkcsError(OpenSSLObjectError):
|
||||||
@@ -259,21 +309,21 @@ class PkcsError(OpenSSLObjectError):
|
|||||||
|
|
||||||
|
|
||||||
class Pkcs(OpenSSLObject):
|
class Pkcs(OpenSSLObject):
|
||||||
|
def __init__(self, module, backend):
|
||||||
def __init__(self, module):
|
|
||||||
super(Pkcs, self).__init__(
|
super(Pkcs, self).__init__(
|
||||||
module.params['path'],
|
module.params['path'],
|
||||||
module.params['state'],
|
module.params['state'],
|
||||||
module.params['force'],
|
module.params['force'],
|
||||||
module.check_mode
|
module.check_mode
|
||||||
)
|
)
|
||||||
|
self.backend = backend
|
||||||
self.action = module.params['action']
|
self.action = module.params['action']
|
||||||
self.other_certificates = module.params['other_certificates']
|
self.other_certificates = module.params['other_certificates']
|
||||||
self.other_certificates_parse_all = module.params['other_certificates_parse_all']
|
self.other_certificates_parse_all = module.params['other_certificates_parse_all']
|
||||||
self.certificate_path = module.params['certificate_path']
|
self.certificate_path = module.params['certificate_path']
|
||||||
self.friendly_name = module.params['friendly_name']
|
self.friendly_name = module.params['friendly_name']
|
||||||
self.iter_size = module.params['iter_size']
|
self.iter_size = module.params['iter_size'] or 2048
|
||||||
self.maciter_size = module.params['maciter_size']
|
self.maciter_size = module.params['maciter_size'] or 1
|
||||||
self.passphrase = module.params['passphrase']
|
self.passphrase = module.params['passphrase']
|
||||||
self.pkcs12 = None
|
self.pkcs12 = None
|
||||||
self.privatekey_passphrase = module.params['privatekey_passphrase']
|
self.privatekey_passphrase = module.params['privatekey_passphrase']
|
||||||
@@ -293,12 +343,37 @@ class Pkcs(OpenSSLObject):
|
|||||||
filenames = list(self.other_certificates)
|
filenames = list(self.other_certificates)
|
||||||
self.other_certificates = []
|
self.other_certificates = []
|
||||||
for other_cert_bundle in filenames:
|
for other_cert_bundle in filenames:
|
||||||
self.other_certificates.extend(load_certificate_set(other_cert_bundle))
|
self.other_certificates.extend(load_certificate_set(other_cert_bundle, self.backend))
|
||||||
else:
|
else:
|
||||||
self.other_certificates = [
|
self.other_certificates = [
|
||||||
load_certificate(other_cert) for other_cert in self.other_certificates
|
load_certificate(other_cert, backend=self.backend) for other_cert in self.other_certificates
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def generate_bytes(self, module):
|
||||||
|
"""Generate PKCS#12 file archive."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def parse_bytes(self, pkcs12_content):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _dump_privatekey(self, pkcs12):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _dump_certificate(self, pkcs12):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _dump_other_certificates(self, pkcs12):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_friendly_name(self, pkcs12):
|
||||||
|
pass
|
||||||
|
|
||||||
def check(self, module, perms_required=True):
|
def check(self, module, perms_required=True):
|
||||||
"""Ensure the resource is in its desired state."""
|
"""Ensure the resource is in its desired state."""
|
||||||
|
|
||||||
@@ -307,10 +382,8 @@ class Pkcs(OpenSSLObject):
|
|||||||
def _check_pkey_passphrase():
|
def _check_pkey_passphrase():
|
||||||
if self.privatekey_passphrase:
|
if self.privatekey_passphrase:
|
||||||
try:
|
try:
|
||||||
load_privatekey(self.privatekey_path, self.privatekey_passphrase)
|
load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend)
|
||||||
except crypto.Error:
|
except OpenSSLObjectError:
|
||||||
return False
|
|
||||||
except OpenSSLBadPassphraseError:
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -318,32 +391,28 @@ class Pkcs(OpenSSLObject):
|
|||||||
return state_and_perms
|
return state_and_perms
|
||||||
|
|
||||||
if os.path.exists(self.path) and module.params['action'] == 'export':
|
if os.path.exists(self.path) and module.params['action'] == 'export':
|
||||||
dummy = self.generate(module)
|
dummy = self.generate_bytes(module)
|
||||||
self.src = self.path
|
self.src = self.path
|
||||||
try:
|
try:
|
||||||
pkcs12_privatekey, pkcs12_certificate, pkcs12_other_certificates, pkcs12_friendly_name = self.parse()
|
pkcs12_privatekey, pkcs12_certificate, pkcs12_other_certificates, pkcs12_friendly_name = self.parse()
|
||||||
except crypto.Error:
|
except OpenSSLObjectError:
|
||||||
return False
|
return False
|
||||||
if (pkcs12_privatekey is not None) and (self.privatekey_path is not None):
|
if (pkcs12_privatekey is not None) and (self.privatekey_path is not None):
|
||||||
expected_pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
expected_pkey = self._dump_privatekey(self.pkcs12)
|
||||||
self.pkcs12.get_privatekey())
|
|
||||||
if pkcs12_privatekey != expected_pkey:
|
if pkcs12_privatekey != expected_pkey:
|
||||||
return False
|
return False
|
||||||
elif bool(pkcs12_privatekey) != bool(self.privatekey_path):
|
elif bool(pkcs12_privatekey) != bool(self.privatekey_path):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (pkcs12_certificate is not None) and (self.certificate_path is not None):
|
if (pkcs12_certificate is not None) and (self.certificate_path is not None):
|
||||||
|
expected_cert = self._dump_certificate(self.pkcs12)
|
||||||
expected_cert = crypto.dump_certificate(crypto.FILETYPE_PEM,
|
|
||||||
self.pkcs12.get_certificate())
|
|
||||||
if pkcs12_certificate != expected_cert:
|
if pkcs12_certificate != expected_cert:
|
||||||
return False
|
return False
|
||||||
elif bool(pkcs12_certificate) != bool(self.certificate_path):
|
elif bool(pkcs12_certificate) != bool(self.certificate_path):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (pkcs12_other_certificates is not None) and (self.other_certificates is not None):
|
if (pkcs12_other_certificates is not None) and (self.other_certificates is not None):
|
||||||
expected_other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM,
|
expected_other_certs = self._dump_other_certificates(self.pkcs12)
|
||||||
other_cert) for other_cert in self.pkcs12.get_ca_certificates()]
|
|
||||||
if set(pkcs12_other_certificates) != set(expected_other_certs):
|
if set(pkcs12_other_certificates) != set(expected_other_certs):
|
||||||
return False
|
return False
|
||||||
elif bool(pkcs12_other_certificates) != bool(self.other_certificates):
|
elif bool(pkcs12_other_certificates) != bool(self.other_certificates):
|
||||||
@@ -352,15 +421,16 @@ class Pkcs(OpenSSLObject):
|
|||||||
if pkcs12_privatekey:
|
if pkcs12_privatekey:
|
||||||
# This check is required because pyOpenSSL will not return a friendly name
|
# This check is required because pyOpenSSL will not return a friendly name
|
||||||
# if the private key is not set in the file
|
# if the private key is not set in the file
|
||||||
if ((self.pkcs12.get_friendlyname() is not None) and (pkcs12_friendly_name is not None)):
|
friendly_name = self._get_friendly_name(self.pkcs12)
|
||||||
if self.pkcs12.get_friendlyname() != pkcs12_friendly_name:
|
if ((friendly_name is not None) and (pkcs12_friendly_name is not None)):
|
||||||
|
if friendly_name != pkcs12_friendly_name:
|
||||||
return False
|
return False
|
||||||
elif bool(self.pkcs12.get_friendlyname()) != bool(pkcs12_friendly_name):
|
elif bool(friendly_name) != bool(pkcs12_friendly_name):
|
||||||
return False
|
return False
|
||||||
elif module.params['action'] == 'parse' and os.path.exists(self.src) and os.path.exists(self.path):
|
elif module.params['action'] == 'parse' and os.path.exists(self.src) and os.path.exists(self.path):
|
||||||
try:
|
try:
|
||||||
pkey, cert, other_certs, friendly_name = self.parse()
|
pkey, cert, other_certs, friendly_name = self.parse()
|
||||||
except crypto.Error:
|
except OpenSSLObjectError:
|
||||||
return False
|
return False
|
||||||
expected_content = to_bytes(
|
expected_content = to_bytes(
|
||||||
''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None])
|
''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None])
|
||||||
@@ -390,27 +460,6 @@ class Pkcs(OpenSSLObject):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def generate(self, module):
|
|
||||||
"""Generate PKCS#12 file archive."""
|
|
||||||
self.pkcs12 = crypto.PKCS12()
|
|
||||||
|
|
||||||
if self.other_certificates:
|
|
||||||
self.pkcs12.set_ca_certificates(self.other_certificates)
|
|
||||||
|
|
||||||
if self.certificate_path:
|
|
||||||
self.pkcs12.set_certificate(load_certificate(self.certificate_path))
|
|
||||||
|
|
||||||
if self.friendly_name:
|
|
||||||
self.pkcs12.set_friendlyname(to_bytes(self.friendly_name))
|
|
||||||
|
|
||||||
if self.privatekey_path:
|
|
||||||
try:
|
|
||||||
self.pkcs12.set_privatekey(load_privatekey(self.privatekey_path, self.privatekey_passphrase))
|
|
||||||
except OpenSSLBadPassphraseError as exc:
|
|
||||||
raise PkcsError(exc)
|
|
||||||
|
|
||||||
return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size)
|
|
||||||
|
|
||||||
def remove(self, module):
|
def remove(self, module):
|
||||||
if self.backup:
|
if self.backup:
|
||||||
self.backup_file = module.backup_local(self.path)
|
self.backup_file = module.backup_local(self.path)
|
||||||
@@ -422,8 +471,51 @@ class Pkcs(OpenSSLObject):
|
|||||||
try:
|
try:
|
||||||
with open(self.src, 'rb') as pkcs12_fh:
|
with open(self.src, 'rb') as pkcs12_fh:
|
||||||
pkcs12_content = pkcs12_fh.read()
|
pkcs12_content = pkcs12_fh.read()
|
||||||
p12 = crypto.load_pkcs12(pkcs12_content,
|
return self.parse_bytes(pkcs12_content)
|
||||||
self.passphrase)
|
except IOError as exc:
|
||||||
|
raise PkcsError(exc)
|
||||||
|
|
||||||
|
def generate(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write(self, module, content, mode=None):
|
||||||
|
"""Write the PKCS#12 file."""
|
||||||
|
if self.backup:
|
||||||
|
self.backup_file = module.backup_local(self.path)
|
||||||
|
write_file(module, content, mode)
|
||||||
|
if self.return_content:
|
||||||
|
self.pkcs12_bytes = content
|
||||||
|
|
||||||
|
|
||||||
|
class PkcsPyOpenSSL(Pkcs):
|
||||||
|
def __init__(self, module):
|
||||||
|
super(PkcsPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||||
|
|
||||||
|
def generate_bytes(self, module):
|
||||||
|
"""Generate PKCS#12 file archive."""
|
||||||
|
self.pkcs12 = crypto.PKCS12()
|
||||||
|
|
||||||
|
if self.other_certificates:
|
||||||
|
self.pkcs12.set_ca_certificates(self.other_certificates)
|
||||||
|
|
||||||
|
if self.certificate_path:
|
||||||
|
self.pkcs12.set_certificate(load_certificate(self.certificate_path, backend=self.backend))
|
||||||
|
|
||||||
|
if self.friendly_name:
|
||||||
|
self.pkcs12.set_friendlyname(to_bytes(self.friendly_name))
|
||||||
|
|
||||||
|
if self.privatekey_path:
|
||||||
|
try:
|
||||||
|
self.pkcs12.set_privatekey(
|
||||||
|
load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend))
|
||||||
|
except OpenSSLBadPassphraseError as exc:
|
||||||
|
raise PkcsError(exc)
|
||||||
|
|
||||||
|
return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size)
|
||||||
|
|
||||||
|
def parse_bytes(self, pkcs12_content):
|
||||||
|
try:
|
||||||
|
p12 = crypto.load_pkcs12(pkcs12_content, self.passphrase)
|
||||||
pkey = p12.get_privatekey()
|
pkey = p12.get_privatekey()
|
||||||
if pkey is not None:
|
if pkey is not None:
|
||||||
pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
|
pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
|
||||||
@@ -438,17 +530,143 @@ class Pkcs(OpenSSLObject):
|
|||||||
friendly_name = p12.get_friendlyname()
|
friendly_name = p12.get_friendlyname()
|
||||||
|
|
||||||
return (pkey, crt, other_certs, friendly_name)
|
return (pkey, crt, other_certs, friendly_name)
|
||||||
|
except crypto.Error as exc:
|
||||||
except IOError as exc:
|
|
||||||
raise PkcsError(exc)
|
raise PkcsError(exc)
|
||||||
|
|
||||||
def write(self, module, content, mode=None):
|
def _dump_privatekey(self, pkcs12):
|
||||||
"""Write the PKCS#12 file."""
|
pk = pkcs12.get_privatekey()
|
||||||
if self.backup:
|
return crypto.dump_privatekey(crypto.FILETYPE_PEM, pk) if pk else None
|
||||||
self.backup_file = module.backup_local(self.path)
|
|
||||||
write_file(module, content, mode)
|
def _dump_certificate(self, pkcs12):
|
||||||
if self.return_content:
|
cert = pkcs12.get_certificate()
|
||||||
self.pkcs12_bytes = content
|
return crypto.dump_certificate(crypto.FILETYPE_PEM, cert) if cert else None
|
||||||
|
|
||||||
|
def _dump_other_certificates(self, pkcs12):
|
||||||
|
return [
|
||||||
|
crypto.dump_certificate(crypto.FILETYPE_PEM, other_cert)
|
||||||
|
for other_cert in pkcs12.get_ca_certificates()
|
||||||
|
]
|
||||||
|
|
||||||
|
def _get_friendly_name(self, pkcs12):
|
||||||
|
return pkcs12.get_friendlyname()
|
||||||
|
|
||||||
|
|
||||||
|
class PkcsCryptography(Pkcs):
|
||||||
|
def __init__(self, module):
|
||||||
|
super(PkcsCryptography, self).__init__(module, 'cryptography')
|
||||||
|
|
||||||
|
def generate_bytes(self, module):
|
||||||
|
"""Generate PKCS#12 file archive."""
|
||||||
|
pkey = None
|
||||||
|
if self.privatekey_path:
|
||||||
|
try:
|
||||||
|
pkey = load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend)
|
||||||
|
except OpenSSLBadPassphraseError as exc:
|
||||||
|
raise PkcsError(exc)
|
||||||
|
|
||||||
|
cert = None
|
||||||
|
if self.certificate_path:
|
||||||
|
cert = load_certificate(self.certificate_path, backend=self.backend)
|
||||||
|
|
||||||
|
friendly_name = to_bytes(self.friendly_name) if self.friendly_name is not None else None
|
||||||
|
|
||||||
|
# Store fake object which can be used to retrieve the components back
|
||||||
|
self.pkcs12 = (pkey, cert, self.other_certificates, friendly_name)
|
||||||
|
|
||||||
|
return serialize_key_and_certificates(
|
||||||
|
friendly_name,
|
||||||
|
pkey,
|
||||||
|
cert,
|
||||||
|
self.other_certificates,
|
||||||
|
serialization.BestAvailableEncryption(to_bytes(self.passphrase))
|
||||||
|
if self.passphrase else serialization.NoEncryption(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_bytes(self, pkcs12_content):
|
||||||
|
try:
|
||||||
|
private_key, certificate, additional_certificates, friendly_name = parse_pkcs12(
|
||||||
|
pkcs12_content, self.passphrase)
|
||||||
|
|
||||||
|
pkey = None
|
||||||
|
if private_key is not None:
|
||||||
|
pkey = private_key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption(),
|
||||||
|
)
|
||||||
|
|
||||||
|
crt = None
|
||||||
|
if certificate is not None:
|
||||||
|
crt = certificate.public_bytes(serialization.Encoding.PEM)
|
||||||
|
|
||||||
|
other_certs = []
|
||||||
|
if additional_certificates is not None:
|
||||||
|
other_certs = [
|
||||||
|
other_cert.public_bytes(serialization.Encoding.PEM)
|
||||||
|
for other_cert in additional_certificates
|
||||||
|
]
|
||||||
|
|
||||||
|
return (pkey, crt, other_certs, friendly_name)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise PkcsError(exc)
|
||||||
|
|
||||||
|
# The following methods will get self.pkcs12 passed, which is computed as:
|
||||||
|
#
|
||||||
|
# self.pkcs12 = (pkey, cert, self.other_certificates, self.friendly_name)
|
||||||
|
|
||||||
|
def _dump_privatekey(self, pkcs12):
|
||||||
|
return pkcs12[0].private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption(),
|
||||||
|
) if pkcs12[0] else None
|
||||||
|
|
||||||
|
def _dump_certificate(self, pkcs12):
|
||||||
|
return pkcs12[1].public_bytes(serialization.Encoding.PEM) if pkcs12[1] else None
|
||||||
|
|
||||||
|
def _dump_other_certificates(self, pkcs12):
|
||||||
|
return [other_cert.public_bytes(serialization.Encoding.PEM) for other_cert in pkcs12[2]]
|
||||||
|
|
||||||
|
def _get_friendly_name(self, pkcs12):
|
||||||
|
return pkcs12[3]
|
||||||
|
|
||||||
|
|
||||||
|
def select_backend(module, backend):
|
||||||
|
if backend == 'auto':
|
||||||
|
# Detection what is possible
|
||||||
|
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
||||||
|
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
||||||
|
|
||||||
|
# If no restrictions are provided, first try cryptography, then pyOpenSSL
|
||||||
|
if module.params['iter_size'] is not None or module.params['maciter_size'] is not None:
|
||||||
|
# If iter_size or maciter_size is specified, use pyOpenSSL backend
|
||||||
|
backend = 'pyopenssl'
|
||||||
|
elif can_use_cryptography:
|
||||||
|
backend = 'cryptography'
|
||||||
|
elif can_use_pyopenssl:
|
||||||
|
backend = 'pyopenssl'
|
||||||
|
|
||||||
|
# Success?
|
||||||
|
if backend == 'auto':
|
||||||
|
module.fail_json(msg=("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='x.0.0', collection_name='community.crypto')
|
||||||
|
return backend, PkcsPyOpenSSL(module)
|
||||||
|
elif backend == 'cryptography':
|
||||||
|
if not CRYPTOGRAPHY_FOUND:
|
||||||
|
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
||||||
|
exception=CRYPTOGRAPHY_IMP_ERR)
|
||||||
|
return backend, PkcsCryptography(module)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unsupported value for backend: {0}'.format(backend))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -459,8 +677,8 @@ def main():
|
|||||||
certificate_path=dict(type='path'),
|
certificate_path=dict(type='path'),
|
||||||
force=dict(type='bool', default=False),
|
force=dict(type='bool', default=False),
|
||||||
friendly_name=dict(type='str', aliases=['name']),
|
friendly_name=dict(type='str', aliases=['name']),
|
||||||
iter_size=dict(type='int', default=2048),
|
iter_size=dict(type='int'),
|
||||||
maciter_size=dict(type='int', default=1),
|
maciter_size=dict(type='int'),
|
||||||
passphrase=dict(type='str', no_log=True),
|
passphrase=dict(type='str', no_log=True),
|
||||||
path=dict(type='path', required=True),
|
path=dict(type='path', required=True),
|
||||||
privatekey_passphrase=dict(type='str', no_log=True),
|
privatekey_passphrase=dict(type='str', no_log=True),
|
||||||
@@ -469,6 +687,7 @@ def main():
|
|||||||
src=dict(type='path'),
|
src=dict(type='path'),
|
||||||
backup=dict(type='bool', default=False),
|
backup=dict(type='bool', default=False),
|
||||||
return_content=dict(type='bool', default=False),
|
return_content=dict(type='bool', default=False),
|
||||||
|
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||||
)
|
)
|
||||||
|
|
||||||
required_if = [
|
required_if = [
|
||||||
@@ -482,8 +701,7 @@ def main():
|
|||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not pyopenssl_found:
|
backend, pkcs12 = select_backend(module, module.params['select_crypto_backend'])
|
||||||
module.fail_json(msg=missing_required_lib('pyOpenSSL'), exception=PYOPENSSL_IMP_ERR)
|
|
||||||
|
|
||||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
base_dir = os.path.dirname(module.params['path']) or '.'
|
||||||
if not os.path.isdir(base_dir):
|
if not os.path.isdir(base_dir):
|
||||||
@@ -493,7 +711,6 @@ def main():
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pkcs12 = Pkcs(module)
|
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
if module.params['state'] == 'present':
|
if module.params['state'] == 'present':
|
||||||
@@ -506,7 +723,7 @@ def main():
|
|||||||
if module.params['action'] == 'export':
|
if module.params['action'] == 'export':
|
||||||
if not module.params['friendly_name']:
|
if not module.params['friendly_name']:
|
||||||
module.fail_json(msg='Friendly_name is required')
|
module.fail_json(msg='Friendly_name is required')
|
||||||
pkcs12_content = pkcs12.generate(module)
|
pkcs12_content = pkcs12.generate_bytes(module)
|
||||||
pkcs12.write(module, pkcs12_content, 0o600)
|
pkcs12.write(module, pkcs12_content, 0o600)
|
||||||
changed = True
|
changed = True
|
||||||
else:
|
else:
|
||||||
@@ -516,7 +733,9 @@ def main():
|
|||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
file_args = module.load_file_common_arguments(module.params)
|
||||||
if module.set_fs_attributes_if_different(file_args, changed):
|
if module.check_file_absent_if_check_mode(file_args['path']):
|
||||||
|
changed = True
|
||||||
|
elif module.set_fs_attributes_if_different(file_args, changed):
|
||||||
changed = True
|
changed = True
|
||||||
else:
|
else:
|
||||||
if module.check_mode:
|
if module.check_mode:
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ privatekey:
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||||
load_file_if_exists,
|
load_file_if_exists,
|
||||||
@@ -214,7 +214,10 @@ class PrivateKeyModule(OpenSSLObject):
|
|||||||
self.changed = True
|
self.changed = True
|
||||||
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
file_args = module.load_file_common_arguments(module.params)
|
||||||
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
|
if module.check_file_absent_if_check_mode(file_args['path']):
|
||||||
|
self.changed = True
|
||||||
|
else:
|
||||||
|
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
|
||||||
|
|
||||||
def remove(self, module):
|
def remove(self, module):
|
||||||
self.module_backend.set_existing(None)
|
self.module_backend.set_existing(None)
|
||||||
|
|||||||
@@ -130,6 +130,62 @@ public_data:
|
|||||||
- Public key data. Depends on key type.
|
- Public key data. Depends on key type.
|
||||||
returned: success
|
returned: success
|
||||||
type: dict
|
type: dict
|
||||||
|
contains:
|
||||||
|
size:
|
||||||
|
description:
|
||||||
|
- Bit size of modulus (RSA) or prime number (DSA).
|
||||||
|
type: int
|
||||||
|
returned: When C(type=RSA) or C(type=DSA)
|
||||||
|
modulus:
|
||||||
|
description:
|
||||||
|
- The RSA key's modulus.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=RSA)
|
||||||
|
exponent:
|
||||||
|
description:
|
||||||
|
- The RSA key's public exponent.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=RSA)
|
||||||
|
p:
|
||||||
|
description:
|
||||||
|
- The C(p) value for DSA.
|
||||||
|
- This is the prime modulus upon which arithmetic takes place.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=DSA)
|
||||||
|
q:
|
||||||
|
description:
|
||||||
|
- The C(q) value for DSA.
|
||||||
|
- This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
|
||||||
|
multiplicative group of the prime field used.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=DSA)
|
||||||
|
g:
|
||||||
|
description:
|
||||||
|
- The C(g) value for DSA.
|
||||||
|
- This is the element spanning the subgroup of the multiplicative group of the prime field used.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=DSA)
|
||||||
|
curve:
|
||||||
|
description:
|
||||||
|
- The curve's name for ECC.
|
||||||
|
type: str
|
||||||
|
returned: When C(type=ECC)
|
||||||
|
exponent_size:
|
||||||
|
description:
|
||||||
|
- The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=ECC)
|
||||||
|
x:
|
||||||
|
description:
|
||||||
|
- The C(x) coordinate for the public point on the elliptic curve.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=ECC)
|
||||||
|
y:
|
||||||
|
description:
|
||||||
|
- For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
|
||||||
|
- For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=DSA) or C(type=ECC)
|
||||||
private_data:
|
private_data:
|
||||||
description:
|
description:
|
||||||
- Private key data. Depends on key type.
|
- Private key data. Depends on key type.
|
||||||
@@ -138,450 +194,19 @@ private_data:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
import abc
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
import os
|
from ansible.module_utils.common.text.converters import to_native
|
||||||
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_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
CRYPTOGRAPHY_HAS_X25519,
|
|
||||||
CRYPTOGRAPHY_HAS_X448,
|
|
||||||
CRYPTOGRAPHY_HAS_ED25519,
|
|
||||||
CRYPTOGRAPHY_HAS_ED448,
|
|
||||||
OpenSSLObjectError,
|
OpenSSLObjectError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import (
|
||||||
OpenSSLObject,
|
PrivateKeyConsistencyError,
|
||||||
load_privatekey,
|
PrivateKeyParseError,
|
||||||
get_fingerprint_of_bytes,
|
select_backend,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
|
|
||||||
binary_exp_mod,
|
|
||||||
quick_is_not_prime,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
import cryptography
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
SIGNATURE_TEST_DATA = b'1234'
|
|
||||||
|
|
||||||
|
|
||||||
def _get_cryptography_key_info(key):
|
|
||||||
key_public_data = dict()
|
|
||||||
key_private_data = dict()
|
|
||||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
|
||||||
key_type = 'RSA'
|
|
||||||
key_public_data['size'] = key.key_size
|
|
||||||
key_public_data['modulus'] = key.public_key().public_numbers().n
|
|
||||||
key_public_data['exponent'] = key.public_key().public_numbers().e
|
|
||||||
key_private_data['p'] = key.private_numbers().p
|
|
||||||
key_private_data['q'] = key.private_numbers().q
|
|
||||||
key_private_data['exponent'] = key.private_numbers().d
|
|
||||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
|
||||||
key_type = 'DSA'
|
|
||||||
key_public_data['size'] = key.key_size
|
|
||||||
key_public_data['p'] = key.parameters().parameter_numbers().p
|
|
||||||
key_public_data['q'] = key.parameters().parameter_numbers().q
|
|
||||||
key_public_data['g'] = key.parameters().parameter_numbers().g
|
|
||||||
key_public_data['y'] = key.public_key().public_numbers().y
|
|
||||||
key_private_data['x'] = key.private_numbers().x
|
|
||||||
elif CRYPTOGRAPHY_HAS_X25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey):
|
|
||||||
key_type = 'X25519'
|
|
||||||
elif CRYPTOGRAPHY_HAS_X448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey):
|
|
||||||
key_type = 'X448'
|
|
||||||
elif CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
|
||||||
key_type = 'Ed25519'
|
|
||||||
elif CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
|
||||||
key_type = 'Ed448'
|
|
||||||
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
|
||||||
key_type = 'ECC'
|
|
||||||
key_public_data['curve'] = key.public_key().curve.name
|
|
||||||
key_public_data['x'] = key.public_key().public_numbers().x
|
|
||||||
key_public_data['y'] = key.public_key().public_numbers().y
|
|
||||||
key_public_data['exponent_size'] = key.public_key().curve.key_size
|
|
||||||
key_private_data['multiplier'] = key.private_numbers().private_value
|
|
||||||
else:
|
|
||||||
key_type = 'unknown ({0})'.format(type(key))
|
|
||||||
return key_type, key_public_data, key_private_data
|
|
||||||
|
|
||||||
|
|
||||||
def _check_dsa_consistency(key_public_data, key_private_data):
|
|
||||||
# Get parameters
|
|
||||||
p = key_public_data.get('p')
|
|
||||||
q = key_public_data.get('q')
|
|
||||||
g = key_public_data.get('g')
|
|
||||||
y = key_public_data.get('y')
|
|
||||||
x = key_private_data.get('x')
|
|
||||||
for v in (p, q, g, y, x):
|
|
||||||
if v is None:
|
|
||||||
return None
|
|
||||||
# Make sure that g is not 0, 1 or -1 in Z/pZ
|
|
||||||
if g < 2 or g >= p - 1:
|
|
||||||
return False
|
|
||||||
# Make sure that x is in range
|
|
||||||
if x < 1 or x >= q:
|
|
||||||
return False
|
|
||||||
# Check whether q divides p-1
|
|
||||||
if (p - 1) % q != 0:
|
|
||||||
return False
|
|
||||||
# Check that g**q mod p == 1
|
|
||||||
if binary_exp_mod(g, q, p) != 1:
|
|
||||||
return False
|
|
||||||
# Check whether g**x mod p == y
|
|
||||||
if binary_exp_mod(g, x, p) != y:
|
|
||||||
return False
|
|
||||||
# Check (quickly) whether p or q are not primes
|
|
||||||
if quick_is_not_prime(q) or quick_is_not_prime(p):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _is_cryptography_key_consistent(key, key_public_data, key_private_data):
|
|
||||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
|
||||||
return bool(key._backend._lib.RSA_check_key(key._rsa_cdata))
|
|
||||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
|
||||||
result = _check_dsa_consistency(key_public_data, key_private_data)
|
|
||||||
if result is not None:
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
signature = key.sign(SIGNATURE_TEST_DATA, cryptography.hazmat.primitives.hashes.SHA256())
|
|
||||||
except AttributeError:
|
|
||||||
# sign() was added in cryptography 1.5, but we support older versions
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
key.public_key().verify(
|
|
||||||
signature,
|
|
||||||
SIGNATURE_TEST_DATA,
|
|
||||||
cryptography.hazmat.primitives.hashes.SHA256()
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
except cryptography.exceptions.InvalidSignature:
|
|
||||||
return False
|
|
||||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
|
||||||
try:
|
|
||||||
signature = key.sign(
|
|
||||||
SIGNATURE_TEST_DATA,
|
|
||||||
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256())
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
# sign() was added in cryptography 1.5, but we support older versions
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
key.public_key().verify(
|
|
||||||
signature,
|
|
||||||
SIGNATURE_TEST_DATA,
|
|
||||||
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256())
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
except cryptography.exceptions.InvalidSignature:
|
|
||||||
return False
|
|
||||||
has_simple_sign_function = False
|
|
||||||
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
|
||||||
has_simple_sign_function = True
|
|
||||||
if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
|
||||||
has_simple_sign_function = True
|
|
||||||
if has_simple_sign_function:
|
|
||||||
signature = key.sign(SIGNATURE_TEST_DATA)
|
|
||||||
try:
|
|
||||||
key.public_key().verify(signature, SIGNATURE_TEST_DATA)
|
|
||||||
return True
|
|
||||||
except cryptography.exceptions.InvalidSignature:
|
|
||||||
return False
|
|
||||||
# For X25519 and X448, there's no test yet.
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class PrivateKeyInfo(OpenSSLObject):
|
|
||||||
def __init__(self, module, backend):
|
|
||||||
super(PrivateKeyInfo, self).__init__(
|
|
||||||
module.params['path'] or '',
|
|
||||||
'present',
|
|
||||||
False,
|
|
||||||
module.check_mode,
|
|
||||||
)
|
|
||||||
self.backend = backend
|
|
||||||
self.module = module
|
|
||||||
self.content = module.params['content']
|
|
||||||
|
|
||||||
self.passphrase = module.params['passphrase']
|
|
||||||
self.return_private_key_data = module.params['return_private_key_data']
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
# Empty method because OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
# Empty method because OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_key_info(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _is_key_consistent(self, key_public_data, key_private_data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_info(self):
|
|
||||||
result = dict(
|
|
||||||
can_load_key=False,
|
|
||||||
can_parse_key=False,
|
|
||||||
key_is_consistent=None,
|
|
||||||
)
|
|
||||||
if self.content is not None:
|
|
||||||
priv_key_detail = self.content.encode('utf-8')
|
|
||||||
result['can_load_key'] = True
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
with open(self.path, 'rb') as b_priv_key_fh:
|
|
||||||
priv_key_detail = b_priv_key_fh.read()
|
|
||||||
result['can_load_key'] = True
|
|
||||||
except (IOError, OSError) as exc:
|
|
||||||
self.module.fail_json(msg=to_native(exc), **result)
|
|
||||||
try:
|
|
||||||
self.key = load_privatekey(
|
|
||||||
path=None,
|
|
||||||
content=priv_key_detail,
|
|
||||||
passphrase=to_bytes(self.passphrase) if self.passphrase is not None else self.passphrase,
|
|
||||||
backend=self.backend
|
|
||||||
)
|
|
||||||
result['can_parse_key'] = True
|
|
||||||
except OpenSSLObjectError as exc:
|
|
||||||
self.module.fail_json(msg=to_native(exc), **result)
|
|
||||||
|
|
||||||
result['public_key'] = self._get_public_key(binary=False)
|
|
||||||
pk = self._get_public_key(binary=True)
|
|
||||||
result['public_key_fingerprints'] = get_fingerprint_of_bytes(pk) if pk is not None else dict()
|
|
||||||
|
|
||||||
key_type, key_public_data, key_private_data = self._get_key_info()
|
|
||||||
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")
|
|
||||||
result['key_is_consistent'] = False
|
|
||||||
self.module.fail_json(
|
|
||||||
msg="Private key is not consistent! (See "
|
|
||||||
"https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)",
|
|
||||||
**result
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class PrivateKeyInfoCryptography(PrivateKeyInfo):
|
|
||||||
"""Validate the supplied private key, using the cryptography backend"""
|
|
||||||
def __init__(self, module):
|
|
||||||
super(PrivateKeyInfoCryptography, self).__init__(module, 'cryptography')
|
|
||||||
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
return self.key.public_key().public_bytes(
|
|
||||||
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
|
||||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_key_info(self):
|
|
||||||
return _get_cryptography_key_info(self.key)
|
|
||||||
|
|
||||||
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 PrivateKeyInfoPyOpenSSL(PrivateKeyInfo):
|
|
||||||
"""validate the supplied private key."""
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(PrivateKeyInfoPyOpenSSL, self).__init__(module, 'pyopenssl')
|
|
||||||
|
|
||||||
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 bigint_to_int(self, 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_key_info(self):
|
|
||||||
key_public_data = dict()
|
|
||||||
key_private_data = dict()
|
|
||||||
openssl_key_type = self.key.type()
|
|
||||||
try_fallback = True
|
|
||||||
if crypto.TYPE_RSA == openssl_key_type:
|
|
||||||
key_type = 'RSA'
|
|
||||||
key_public_data['size'] = self.key.bits()
|
|
||||||
|
|
||||||
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_public_data['modulus'] = self.bigint_to_int(n[0])
|
|
||||||
key_public_data['exponent'] = self.bigint_to_int(e[0])
|
|
||||||
key_private_data['exponent'] = self.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'] = self.bigint_to_int(p[0])
|
|
||||||
key_private_data['q'] = self.bigint_to_int(q[0])
|
|
||||||
else:
|
|
||||||
# Get modulus and exponents
|
|
||||||
key_public_data['modulus'] = self.bigint_to_int(key.n)
|
|
||||||
key_public_data['exponent'] = self.bigint_to_int(key.e)
|
|
||||||
key_private_data['exponent'] = self.bigint_to_int(key.d)
|
|
||||||
# Get factors
|
|
||||||
key_private_data['p'] = self.bigint_to_int(key.p)
|
|
||||||
key_private_data['q'] = self.bigint_to_int(key.q)
|
|
||||||
try_fallback = False
|
|
||||||
except AttributeError:
|
|
||||||
# Use fallback if available
|
|
||||||
pass
|
|
||||||
elif crypto.TYPE_DSA == openssl_key_type:
|
|
||||||
key_type = 'DSA'
|
|
||||||
key_public_data['size'] = self.key.bits()
|
|
||||||
|
|
||||||
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 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'] = self.bigint_to_int(p[0])
|
|
||||||
key_public_data['q'] = self.bigint_to_int(q[0])
|
|
||||||
key_public_data['g'] = self.bigint_to_int(g[0])
|
|
||||||
# Get public and 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_public_data['y'] = self.bigint_to_int(y[0])
|
|
||||||
key_private_data['x'] = self.bigint_to_int(x[0])
|
|
||||||
else:
|
|
||||||
# Get public parameters (primes and group element)
|
|
||||||
key_public_data['p'] = self.bigint_to_int(key.p)
|
|
||||||
key_public_data['q'] = self.bigint_to_int(key.q)
|
|
||||||
key_public_data['g'] = self.bigint_to_int(key.g)
|
|
||||||
# Get public and private key exponents
|
|
||||||
key_public_data['y'] = self.bigint_to_int(key.pub_key)
|
|
||||||
key_private_data['x'] = self.bigint_to_int(key.priv_key)
|
|
||||||
try_fallback = False
|
|
||||||
except AttributeError:
|
|
||||||
# Use fallback if available
|
|
||||||
pass
|
|
||||||
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_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 main():
|
def main():
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
@@ -601,49 +226,39 @@ def main():
|
|||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
result = dict(
|
||||||
|
can_load_key=False,
|
||||||
|
can_parse_key=False,
|
||||||
|
key_is_consistent=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if module.params['content'] is not None:
|
||||||
|
data = module.params['content'].encode('utf-8')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with open(module.params['path'], 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
module.fail_json(msg='Error while reading private key file from disk: {0}'.format(e), **result)
|
||||||
|
|
||||||
|
result['can_load_key'] = True
|
||||||
|
|
||||||
|
backend, module_backend = select_backend(
|
||||||
|
module,
|
||||||
|
module.params['select_crypto_backend'],
|
||||||
|
data,
|
||||||
|
passphrase=module.params['passphrase'],
|
||||||
|
return_private_key_data=module.params['return_private_key_data'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if module.params['path'] is not None:
|
result.update(module_backend.get_info())
|
||||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
|
||||||
if not os.path.isdir(base_dir):
|
|
||||||
module.fail_json(
|
|
||||||
name=base_dir,
|
|
||||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
backend = module.params['select_crypto_backend']
|
|
||||||
if backend == 'auto':
|
|
||||||
# Detect what backend we can use
|
|
||||||
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
|
|
||||||
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
|
|
||||||
|
|
||||||
# If cryptography is available we'll use it
|
|
||||||
if can_use_cryptography:
|
|
||||||
backend = 'cryptography'
|
|
||||||
elif can_use_pyopenssl:
|
|
||||||
backend = 'pyopenssl'
|
|
||||||
|
|
||||||
# Fail if no backend has been found
|
|
||||||
if backend == 'auto':
|
|
||||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
|
||||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
|
||||||
MINIMAL_PYOPENSSL_VERSION))
|
|
||||||
|
|
||||||
if backend == 'pyopenssl':
|
|
||||||
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')
|
|
||||||
privatekey = PrivateKeyInfoPyOpenSSL(module)
|
|
||||||
elif backend == 'cryptography':
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
privatekey = PrivateKeyInfoCryptography(module)
|
|
||||||
|
|
||||||
result = privatekey.get_info()
|
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
|
except PrivateKeyParseError as exc:
|
||||||
|
result.update(exc.result)
|
||||||
|
module.fail_json(msg=exc.error_message, **result)
|
||||||
|
except PrivateKeyConsistencyError as exc:
|
||||||
|
result.update(exc.result)
|
||||||
|
module.fail_json(msg=exc.error_message, **result)
|
||||||
except OpenSSLObjectError as exc:
|
except OpenSSLObjectError as exc:
|
||||||
module.fail_json(msg=to_native(exc))
|
module.fail_json(msg=to_native(exc))
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ EXAMPLES = r'''
|
|||||||
no_log: true # make sure that private key data is not accidentally revealed in logs!
|
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
|
- name: Update encrypted key when openssl_privatekey_pipe reported a change
|
||||||
community.sops.encrypt_sops:
|
community.sops.sops_encrypt:
|
||||||
path: private_key.pem.sops
|
path: private_key.pem.sops
|
||||||
content_text: "{{ output.privatekey }}"
|
content_text: "{{ output.privatekey }}"
|
||||||
when: output is changed
|
when: output is changed
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ import traceback
|
|||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||||
load_file_if_exists,
|
load_file_if_exists,
|
||||||
@@ -203,6 +203,11 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
|||||||
get_fingerprint,
|
get_fingerprint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||||
|
PublicKeyParseError,
|
||||||
|
get_publickey_info,
|
||||||
|
)
|
||||||
|
|
||||||
MINIMAL_PYOPENSSL_VERSION = '16.0.0'
|
MINIMAL_PYOPENSSL_VERSION = '16.0.0'
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH = '1.4'
|
MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH = '1.4'
|
||||||
@@ -244,6 +249,7 @@ class PublicKey(OpenSSLObject):
|
|||||||
module.params['force'],
|
module.params['force'],
|
||||||
module.check_mode
|
module.check_mode
|
||||||
)
|
)
|
||||||
|
self.module = module
|
||||||
self.format = module.params['format']
|
self.format = module.params['format']
|
||||||
self.privatekey_path = module.params['privatekey_path']
|
self.privatekey_path = module.params['privatekey_path']
|
||||||
self.privatekey_content = module.params['privatekey_content']
|
self.privatekey_content = module.params['privatekey_content']
|
||||||
@@ -259,6 +265,23 @@ class PublicKey(OpenSSLObject):
|
|||||||
self.backup = module.params['backup']
|
self.backup = module.params['backup']
|
||||||
self.backup_file = None
|
self.backup_file = None
|
||||||
|
|
||||||
|
self.diff_before = self._get_info(None)
|
||||||
|
self.diff_after = self._get_info(None)
|
||||||
|
|
||||||
|
def _get_info(self, data):
|
||||||
|
if data is None:
|
||||||
|
return dict()
|
||||||
|
result = dict(can_parse_key=False)
|
||||||
|
try:
|
||||||
|
result.update(get_publickey_info(
|
||||||
|
self.module, self.backend, content=data, prefer_one_fingerprint=True))
|
||||||
|
result['can_parse_key'] = True
|
||||||
|
except PublicKeyParseError as exc:
|
||||||
|
result.update(exc.result)
|
||||||
|
except Exception as exc:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
def _create_publickey(self, module):
|
def _create_publickey(self, module):
|
||||||
self.privatekey = load_privatekey(
|
self.privatekey = load_privatekey(
|
||||||
path=self.privatekey_path,
|
path=self.privatekey_path,
|
||||||
@@ -294,6 +317,7 @@ class PublicKey(OpenSSLObject):
|
|||||||
if not self.check(module, perms_required=False) or self.force:
|
if not self.check(module, perms_required=False) or self.force:
|
||||||
try:
|
try:
|
||||||
publickey_content = self._create_publickey(module)
|
publickey_content = self._create_publickey(module)
|
||||||
|
self.diff_after = self._get_info(publickey_content)
|
||||||
if self.return_content:
|
if self.return_content:
|
||||||
self.publickey_bytes = publickey_content
|
self.publickey_bytes = publickey_content
|
||||||
|
|
||||||
@@ -314,7 +338,9 @@ class PublicKey(OpenSSLObject):
|
|||||||
backend=self.backend,
|
backend=self.backend,
|
||||||
)
|
)
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
file_args = module.load_file_common_arguments(module.params)
|
||||||
if module.set_fs_attributes_if_different(file_args, False):
|
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
|
self.changed = True
|
||||||
|
|
||||||
def check(self, module, perms_required=True):
|
def check(self, module, perms_required=True):
|
||||||
@@ -329,6 +355,7 @@ class PublicKey(OpenSSLObject):
|
|||||||
try:
|
try:
|
||||||
with open(self.path, 'rb') as public_key_fh:
|
with open(self.path, 'rb') as public_key_fh:
|
||||||
publickey_content = public_key_fh.read()
|
publickey_content = public_key_fh.read()
|
||||||
|
self.diff_before = self.diff_after = self._get_info(publickey_content)
|
||||||
if self.return_content:
|
if self.return_content:
|
||||||
self.publickey_bytes = publickey_content
|
self.publickey_bytes = publickey_content
|
||||||
if self.backend == 'cryptography':
|
if self.backend == 'cryptography':
|
||||||
@@ -387,6 +414,11 @@ class PublicKey(OpenSSLObject):
|
|||||||
self.publickey_bytes = load_file_if_exists(self.path, ignore_errors=True)
|
self.publickey_bytes = load_file_if_exists(self.path, ignore_errors=True)
|
||||||
result['publickey'] = self.publickey_bytes.decode('utf-8') if self.publickey_bytes else None
|
result['publickey'] = self.publickey_bytes.decode('utf-8') if self.publickey_bytes else None
|
||||||
|
|
||||||
|
result['diff'] = dict(
|
||||||
|
before=self.diff_before,
|
||||||
|
after=self.diff_after,
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
219
plugins/modules/openssl_publickey_info.py
Normal file
219
plugins/modules/openssl_publickey_info.py
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2021, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: openssl_publickey_info
|
||||||
|
short_description: Provide information for OpenSSL public keys
|
||||||
|
description:
|
||||||
|
- This module allows one to query information on OpenSSL public keys.
|
||||||
|
- It uses the 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.
|
||||||
|
version_added: 1.7.0
|
||||||
|
requirements:
|
||||||
|
- PyOpenSSL >= 0.15 or cryptography >= 1.2.3
|
||||||
|
author:
|
||||||
|
- Felix Fontein (@felixfontein)
|
||||||
|
options:
|
||||||
|
path:
|
||||||
|
description:
|
||||||
|
- Remote absolute path where the public key file is loaded from.
|
||||||
|
type: path
|
||||||
|
content:
|
||||||
|
description:
|
||||||
|
- Content of the public key file.
|
||||||
|
- Either I(path) or I(content) must be specified, but not both.
|
||||||
|
type: str
|
||||||
|
|
||||||
|
select_crypto_backend:
|
||||||
|
description:
|
||||||
|
- Determines which crypto backend to use.
|
||||||
|
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
|
||||||
|
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
|
||||||
|
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
|
||||||
|
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
|
||||||
|
From that point on, only the C(cryptography) backend will be available.
|
||||||
|
type: str
|
||||||
|
default: auto
|
||||||
|
choices: [ auto, cryptography, pyopenssl ]
|
||||||
|
|
||||||
|
notes:
|
||||||
|
- Supports C(check_mode).
|
||||||
|
|
||||||
|
seealso:
|
||||||
|
- module: community.crypto.openssl_publickey
|
||||||
|
- module: community.crypto.openssl_privatekey_info
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Generate an OpenSSL private key with the default values (4096 bits, RSA)
|
||||||
|
community.crypto.openssl_privatekey:
|
||||||
|
path: /etc/ssl/private/ansible.com.pem
|
||||||
|
|
||||||
|
- name: Create public key from private key
|
||||||
|
community.crypto.openssl_privatekey:
|
||||||
|
privatekey_path: /etc/ssl/private/ansible.com.pem
|
||||||
|
path: /etc/ssl/ansible.com.pub
|
||||||
|
|
||||||
|
- name: Get information on public key
|
||||||
|
community.crypto.openssl_publickey_info:
|
||||||
|
path: /etc/ssl/ansible.com.pub
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Dump information
|
||||||
|
ansible.builtin.debug:
|
||||||
|
var: result
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
fingerprints:
|
||||||
|
description:
|
||||||
|
- Fingerprints of public key.
|
||||||
|
- For every hash algorithm available, the fingerprint is computed.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
|
||||||
|
'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
|
||||||
|
type:
|
||||||
|
description:
|
||||||
|
- The key's type.
|
||||||
|
- One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
|
||||||
|
- Will start with C(unknown) if the key type cannot be determined.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
sample: RSA
|
||||||
|
public_data:
|
||||||
|
description:
|
||||||
|
- Public key data. Depends on key type.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
contains:
|
||||||
|
size:
|
||||||
|
description:
|
||||||
|
- Bit size of modulus (RSA) or prime number (DSA).
|
||||||
|
type: int
|
||||||
|
returned: When C(type=RSA) or C(type=DSA)
|
||||||
|
modulus:
|
||||||
|
description:
|
||||||
|
- The RSA key's modulus.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=RSA)
|
||||||
|
exponent:
|
||||||
|
description:
|
||||||
|
- The RSA key's public exponent.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=RSA)
|
||||||
|
p:
|
||||||
|
description:
|
||||||
|
- The C(p) value for DSA.
|
||||||
|
- This is the prime modulus upon which arithmetic takes place.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=DSA)
|
||||||
|
q:
|
||||||
|
description:
|
||||||
|
- The C(q) value for DSA.
|
||||||
|
- This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
|
||||||
|
multiplicative group of the prime field used.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=DSA)
|
||||||
|
g:
|
||||||
|
description:
|
||||||
|
- The C(g) value for DSA.
|
||||||
|
- This is the element spanning the subgroup of the multiplicative group of the prime field used.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=DSA)
|
||||||
|
curve:
|
||||||
|
description:
|
||||||
|
- The curve's name for ECC.
|
||||||
|
type: str
|
||||||
|
returned: When C(type=ECC)
|
||||||
|
exponent_size:
|
||||||
|
description:
|
||||||
|
- The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=ECC)
|
||||||
|
x:
|
||||||
|
description:
|
||||||
|
- The C(x) coordinate for the public point on the elliptic curve.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=ECC)
|
||||||
|
y:
|
||||||
|
description:
|
||||||
|
- For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
|
||||||
|
- For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
|
||||||
|
type: int
|
||||||
|
returned: When C(type=DSA) or C(type=ECC)
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
from ansible.module_utils.common.text.converters import to_native
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
|
OpenSSLObjectError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
|
||||||
|
PublicKeyParseError,
|
||||||
|
select_backend,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
path=dict(type='path'),
|
||||||
|
content=dict(type='str', no_log=True),
|
||||||
|
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
|
||||||
|
),
|
||||||
|
required_one_of=(
|
||||||
|
['path', 'content'],
|
||||||
|
),
|
||||||
|
mutually_exclusive=(
|
||||||
|
['path', 'content'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = dict(
|
||||||
|
can_load_key=False,
|
||||||
|
can_parse_key=False,
|
||||||
|
key_is_consistent=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if module.params['content'] is not None:
|
||||||
|
data = module.params['content'].encode('utf-8')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with open(module.params['path'], 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
module.fail_json(msg='Error while reading public key file from disk: {0}'.format(e), **result)
|
||||||
|
|
||||||
|
backend, module_backend = select_backend(
|
||||||
|
module,
|
||||||
|
module.params['select_crypto_backend'],
|
||||||
|
data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result.update(module_backend.get_info())
|
||||||
|
module.exit_json(**result)
|
||||||
|
except PublicKeyParseError as exc:
|
||||||
|
result.update(exc.result)
|
||||||
|
module.fail_json(msg=exc.error_message, **result)
|
||||||
|
except OpenSSLObjectError as exc:
|
||||||
|
module.fail_json(msg=to_native(exc))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -139,7 +139,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
|||||||
load_privatekey,
|
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
|
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
|||||||
load_certificate,
|
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
|
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ certificate:
|
|||||||
|
|
||||||
import os
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||||
select_backend,
|
select_backend,
|
||||||
@@ -472,7 +472,10 @@ class GenericCertificate(OpenSSLObject):
|
|||||||
self.changed = True
|
self.changed = True
|
||||||
|
|
||||||
file_args = module.load_file_common_arguments(module.params)
|
file_args = module.load_file_common_arguments(module.params)
|
||||||
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
|
if module.check_file_absent_if_check_mode(file_args['path']):
|
||||||
|
self.changed = True
|
||||||
|
else:
|
||||||
|
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
|
||||||
|
|
||||||
def check(self, module, perms_required=True):
|
def check(self, module, perms_required=True):
|
||||||
"""Ensure the resource is in its desired state."""
|
"""Ensure the resource is in its desired state."""
|
||||||
|
|||||||
@@ -227,6 +227,77 @@ public_key:
|
|||||||
returned: success
|
returned: success
|
||||||
type: str
|
type: str
|
||||||
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
|
||||||
|
public_key_type:
|
||||||
|
description:
|
||||||
|
- The certificate's public key's type.
|
||||||
|
- One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
|
||||||
|
- Will start with C(unknown) if the key type cannot be determined.
|
||||||
|
returned: success
|
||||||
|
type: str
|
||||||
|
version_added: 1.7.0
|
||||||
|
sample: RSA
|
||||||
|
public_key_data:
|
||||||
|
description:
|
||||||
|
- Public key data. Depends on the public key's type.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
version_added: 1.7.0
|
||||||
|
contains:
|
||||||
|
size:
|
||||||
|
description:
|
||||||
|
- Bit size of modulus (RSA) or prime number (DSA).
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=RSA) or C(public_key_type=DSA)
|
||||||
|
modulus:
|
||||||
|
description:
|
||||||
|
- The RSA key's modulus.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=RSA)
|
||||||
|
exponent:
|
||||||
|
description:
|
||||||
|
- The RSA key's public exponent.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=RSA)
|
||||||
|
p:
|
||||||
|
description:
|
||||||
|
- The C(p) value for DSA.
|
||||||
|
- This is the prime modulus upon which arithmetic takes place.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=DSA)
|
||||||
|
q:
|
||||||
|
description:
|
||||||
|
- The C(q) value for DSA.
|
||||||
|
- This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
|
||||||
|
multiplicative group of the prime field used.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=DSA)
|
||||||
|
g:
|
||||||
|
description:
|
||||||
|
- The C(g) value for DSA.
|
||||||
|
- This is the element spanning the subgroup of the multiplicative group of the prime field used.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=DSA)
|
||||||
|
curve:
|
||||||
|
description:
|
||||||
|
- The curve's name for ECC.
|
||||||
|
type: str
|
||||||
|
returned: When C(public_key_type=ECC)
|
||||||
|
exponent_size:
|
||||||
|
description:
|
||||||
|
- The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=ECC)
|
||||||
|
x:
|
||||||
|
description:
|
||||||
|
- The C(x) coordinate for the public point on the elliptic curve.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=ECC)
|
||||||
|
y:
|
||||||
|
description:
|
||||||
|
- For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
|
||||||
|
- For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
|
||||||
|
type: int
|
||||||
|
returned: When C(public_key_type=DSA) or C(public_key_type=ECC)
|
||||||
public_key_fingerprints:
|
public_key_fingerprints:
|
||||||
description:
|
description:
|
||||||
- Fingerprints of certificate's public key.
|
- Fingerprints of certificate's public key.
|
||||||
@@ -304,529 +375,22 @@ ocsp_uri:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
import abc
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
import binascii
|
|
||||||
import datetime
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
from ansible.module_utils.six import string_types
|
from ansible.module_utils.six import string_types
|
||||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
from ansible.module_utils.common.text.converters import to_native
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
|
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
OpenSSLObjectError,
|
OpenSSLObjectError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||||
OpenSSLObject,
|
|
||||||
get_relative_time_option,
|
get_relative_time_option,
|
||||||
load_certificate,
|
|
||||||
get_fingerprint_of_bytes,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
|
||||||
cryptography_decode_name,
|
select_backend,
|
||||||
cryptography_get_extensions_from_cert,
|
|
||||||
cryptography_oid_to_name,
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
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:
|
|
||||||
import cryptography
|
|
||||||
from cryptography import x509
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
|
|
||||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateInfo(OpenSSLObject):
|
|
||||||
def __init__(self, module, backend):
|
|
||||||
super(CertificateInfo, self).__init__(
|
|
||||||
module.params['path'] or '',
|
|
||||||
'present',
|
|
||||||
False,
|
|
||||||
module.check_mode,
|
|
||||||
)
|
|
||||||
self.backend = backend
|
|
||||||
self.module = module
|
|
||||||
self.content = module.params['content']
|
|
||||||
if self.content is not None:
|
|
||||||
self.content = self.content.encode('utf-8')
|
|
||||||
|
|
||||||
self.valid_at = module.params['valid_at']
|
|
||||||
if self.valid_at:
|
|
||||||
for k, v in self.valid_at.items():
|
|
||||||
if not isinstance(v, string_types):
|
|
||||||
self.module.fail_json(
|
|
||||||
msg='The value for valid_at.{0} must be of type string (got {1})'.format(k, type(v))
|
|
||||||
)
|
|
||||||
self.valid_at[k] = get_relative_time_option(v, 'valid_at.{0}'.format(k))
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
# Empty method because OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
# Empty method because OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_der_bytes(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_signature_algorithm(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_subject_ordered(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_issuer_ordered(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_version(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_key_usage(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_extended_key_usage(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_basic_constraints(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_ocsp_must_staple(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_subject_alt_name(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_not_before(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_not_after(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_subject_key_identifier(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_authority_key_identifier(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_serial_number(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_all_extensions(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _get_ocsp_uri(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_info(self):
|
|
||||||
result = dict()
|
|
||||||
self.cert = load_certificate(self.path, content=self.content, backend=self.backend)
|
|
||||||
|
|
||||||
result['signature_algorithm'] = self._get_signature_algorithm()
|
|
||||||
subject = self._get_subject_ordered()
|
|
||||||
issuer = self._get_issuer_ordered()
|
|
||||||
result['subject'] = dict()
|
|
||||||
for k, v in subject:
|
|
||||||
result['subject'][k] = v
|
|
||||||
result['subject_ordered'] = subject
|
|
||||||
result['issuer'] = dict()
|
|
||||||
for k, v in issuer:
|
|
||||||
result['issuer'][k] = v
|
|
||||||
result['issuer_ordered'] = issuer
|
|
||||||
result['version'] = self._get_version()
|
|
||||||
result['key_usage'], result['key_usage_critical'] = self._get_key_usage()
|
|
||||||
result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage()
|
|
||||||
result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints()
|
|
||||||
result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple()
|
|
||||||
result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name()
|
|
||||||
|
|
||||||
not_before = self._get_not_before()
|
|
||||||
not_after = self._get_not_after()
|
|
||||||
result['not_before'] = not_before.strftime(TIMESTAMP_FORMAT)
|
|
||||||
result['not_after'] = not_after.strftime(TIMESTAMP_FORMAT)
|
|
||||||
result['expired'] = not_after < datetime.datetime.utcnow()
|
|
||||||
|
|
||||||
result['valid_at'] = dict()
|
|
||||||
if self.valid_at:
|
|
||||||
for k, v in self.valid_at.items():
|
|
||||||
result['valid_at'][k] = not_before <= v <= not_after
|
|
||||||
|
|
||||||
result['public_key'] = self._get_public_key(binary=False)
|
|
||||||
pk = self._get_public_key(binary=True)
|
|
||||||
result['public_key_fingerprints'] = get_fingerprint_of_bytes(pk) if pk is not None else dict()
|
|
||||||
|
|
||||||
result['fingerprints'] = get_fingerprint_of_bytes(self._get_der_bytes())
|
|
||||||
|
|
||||||
if self.backend != 'pyopenssl':
|
|
||||||
ski = self._get_subject_key_identifier()
|
|
||||||
if ski is not None:
|
|
||||||
ski = to_native(binascii.hexlify(ski))
|
|
||||||
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
|
|
||||||
result['subject_key_identifier'] = ski
|
|
||||||
|
|
||||||
aki, aci, acsn = self._get_authority_key_identifier()
|
|
||||||
if aki is not None:
|
|
||||||
aki = to_native(binascii.hexlify(aki))
|
|
||||||
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
|
|
||||||
result['authority_key_identifier'] = aki
|
|
||||||
result['authority_cert_issuer'] = aci
|
|
||||||
result['authority_cert_serial_number'] = acsn
|
|
||||||
|
|
||||||
result['serial_number'] = self._get_serial_number()
|
|
||||||
result['extensions_by_oid'] = self._get_all_extensions()
|
|
||||||
result['ocsp_uri'] = self._get_ocsp_uri()
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateInfoCryptography(CertificateInfo):
|
|
||||||
"""Validate the supplied cert, using the cryptography backend"""
|
|
||||||
def __init__(self, module):
|
|
||||||
super(CertificateInfoCryptography, self).__init__(module, 'cryptography')
|
|
||||||
|
|
||||||
def _get_der_bytes(self):
|
|
||||||
return self.cert.public_bytes(serialization.Encoding.DER)
|
|
||||||
|
|
||||||
def _get_signature_algorithm(self):
|
|
||||||
return cryptography_oid_to_name(self.cert.signature_algorithm_oid)
|
|
||||||
|
|
||||||
def _get_subject_ordered(self):
|
|
||||||
result = []
|
|
||||||
for attribute in self.cert.subject:
|
|
||||||
result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _get_issuer_ordered(self):
|
|
||||||
result = []
|
|
||||||
for attribute in self.cert.issuer:
|
|
||||||
result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _get_version(self):
|
|
||||||
if self.cert.version == x509.Version.v1:
|
|
||||||
return 1
|
|
||||||
if self.cert.version == x509.Version.v3:
|
|
||||||
return 3
|
|
||||||
return "unknown"
|
|
||||||
|
|
||||||
def _get_key_usage(self):
|
|
||||||
try:
|
|
||||||
current_key_ext = self.cert.extensions.get_extension_for_class(x509.KeyUsage)
|
|
||||||
current_key_usage = current_key_ext.value
|
|
||||||
key_usage = dict(
|
|
||||||
digital_signature=current_key_usage.digital_signature,
|
|
||||||
content_commitment=current_key_usage.content_commitment,
|
|
||||||
key_encipherment=current_key_usage.key_encipherment,
|
|
||||||
data_encipherment=current_key_usage.data_encipherment,
|
|
||||||
key_agreement=current_key_usage.key_agreement,
|
|
||||||
key_cert_sign=current_key_usage.key_cert_sign,
|
|
||||||
crl_sign=current_key_usage.crl_sign,
|
|
||||||
encipher_only=False,
|
|
||||||
decipher_only=False,
|
|
||||||
)
|
|
||||||
if key_usage['key_agreement']:
|
|
||||||
key_usage.update(dict(
|
|
||||||
encipher_only=current_key_usage.encipher_only,
|
|
||||||
decipher_only=current_key_usage.decipher_only
|
|
||||||
))
|
|
||||||
|
|
||||||
key_usage_names = dict(
|
|
||||||
digital_signature='Digital Signature',
|
|
||||||
content_commitment='Non Repudiation',
|
|
||||||
key_encipherment='Key Encipherment',
|
|
||||||
data_encipherment='Data Encipherment',
|
|
||||||
key_agreement='Key Agreement',
|
|
||||||
key_cert_sign='Certificate Sign',
|
|
||||||
crl_sign='CRL Sign',
|
|
||||||
encipher_only='Encipher Only',
|
|
||||||
decipher_only='Decipher Only',
|
|
||||||
)
|
|
||||||
return sorted([
|
|
||||||
key_usage_names[name] for name, value in key_usage.items() if value
|
|
||||||
]), current_key_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_extended_key_usage(self):
|
|
||||||
try:
|
|
||||||
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
|
|
||||||
return sorted([
|
|
||||||
cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value
|
|
||||||
]), ext_keyusage_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_basic_constraints(self):
|
|
||||||
try:
|
|
||||||
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.BasicConstraints)
|
|
||||||
result = []
|
|
||||||
result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE'))
|
|
||||||
if ext_keyusage_ext.value.path_length is not None:
|
|
||||||
result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length))
|
|
||||||
return sorted(result), ext_keyusage_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_ocsp_must_staple(self):
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
# This only works with cryptography >= 2.1
|
|
||||||
tlsfeature_ext = self.cert.extensions.get_extension_for_class(x509.TLSFeature)
|
|
||||||
value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
|
|
||||||
except AttributeError as dummy:
|
|
||||||
# Fallback for cryptography < 2.1
|
|
||||||
oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
|
|
||||||
tlsfeature_ext = self.cert.extensions.get_extension_for_oid(oid)
|
|
||||||
value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05"
|
|
||||||
return value, tlsfeature_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_subject_alt_name(self):
|
|
||||||
try:
|
|
||||||
san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
|
||||||
result = [cryptography_decode_name(san) for san in san_ext.value]
|
|
||||||
return result, san_ext.critical
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def _get_not_before(self):
|
|
||||||
return self.cert.not_valid_before
|
|
||||||
|
|
||||||
def _get_not_after(self):
|
|
||||||
return self.cert.not_valid_after
|
|
||||||
|
|
||||||
def _get_public_key(self, binary):
|
|
||||||
return self.cert.public_key().public_bytes(
|
|
||||||
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
|
||||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_subject_key_identifier(self):
|
|
||||||
try:
|
|
||||||
ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
|
|
||||||
return ext.value.digest
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_authority_key_identifier(self):
|
|
||||||
try:
|
|
||||||
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
|
||||||
issuer = None
|
|
||||||
if ext.value.authority_cert_issuer is not None:
|
|
||||||
issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
|
|
||||||
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
|
||||||
except cryptography.x509.ExtensionNotFound:
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
def _get_serial_number(self):
|
|
||||||
return cryptography_serial_number_of_cert(self.cert)
|
|
||||||
|
|
||||||
def _get_all_extensions(self):
|
|
||||||
return cryptography_get_extensions_from_cert(self.cert)
|
|
||||||
|
|
||||||
def _get_ocsp_uri(self):
|
|
||||||
try:
|
|
||||||
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess)
|
|
||||||
for desc in ext.value:
|
|
||||||
if desc.access_method == x509.oid.AuthorityInformationAccessOID.OCSP:
|
|
||||||
if isinstance(desc.access_location, x509.UniformResourceIdentifier):
|
|
||||||
return desc.access_location.value
|
|
||||||
except x509.ExtensionNotFound as dummy:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateInfoPyOpenSSL(CertificateInfo):
|
|
||||||
"""validate the supplied certificate."""
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(CertificateInfoPyOpenSSL, self).__init__(module, 'pyopenssl')
|
|
||||||
|
|
||||||
def _get_der_bytes(self):
|
|
||||||
return crypto.dump_certificate(crypto.FILETYPE_ASN1, self.cert)
|
|
||||||
|
|
||||||
def _get_signature_algorithm(self):
|
|
||||||
return to_text(self.cert.get_signature_algorithm())
|
|
||||||
|
|
||||||
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(self, binary):
|
|
||||||
try:
|
|
||||||
return crypto.dump_publickey(
|
|
||||||
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
|
|
||||||
self.cert.get_pubkey()
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
# pyOpenSSL < 16.0:
|
|
||||||
bio = crypto._new_mem_buf()
|
|
||||||
if binary:
|
|
||||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.cert.get_pubkey()._pkey)
|
|
||||||
else:
|
|
||||||
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_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 main():
|
def main():
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
@@ -848,53 +412,37 @@ def main():
|
|||||||
module.deprecate("The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_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')
|
version='2.0.0', collection_name='community.crypto')
|
||||||
|
|
||||||
try:
|
if module.params['content'] is not None:
|
||||||
if module.params['path'] is not None:
|
data = module.params['content'].encode('utf-8')
|
||||||
base_dir = os.path.dirname(module.params['path']) or '.'
|
else:
|
||||||
if not os.path.isdir(base_dir):
|
try:
|
||||||
|
with open(module.params['path'], 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
module.fail_json(msg='Error while reading certificate file from disk: {0}'.format(e))
|
||||||
|
|
||||||
|
backend, module_backend = select_backend(module, module.params['select_crypto_backend'], data)
|
||||||
|
|
||||||
|
valid_at = module.params['valid_at']
|
||||||
|
if valid_at:
|
||||||
|
for k, v in valid_at.items():
|
||||||
|
if not isinstance(v, string_types):
|
||||||
module.fail_json(
|
module.fail_json(
|
||||||
name=base_dir,
|
msg='The value for valid_at.{0} must be of type string (got {1})'.format(k, type(v))
|
||||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
|
||||||
)
|
)
|
||||||
|
valid_at[k] = get_relative_time_option(v, 'valid_at.{0}'.format(k))
|
||||||
|
|
||||||
backend = module.params['select_crypto_backend']
|
try:
|
||||||
if backend == 'auto':
|
result = module_backend.get_info()
|
||||||
# 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
|
not_before = module_backend.get_not_before()
|
||||||
if can_use_cryptography:
|
not_after = module_backend.get_not_after()
|
||||||
backend = 'cryptography'
|
|
||||||
elif can_use_pyopenssl:
|
|
||||||
backend = 'pyopenssl'
|
|
||||||
|
|
||||||
# Fail if no backend has been found
|
result['valid_at'] = dict()
|
||||||
if backend == 'auto':
|
if valid_at:
|
||||||
module.fail_json(msg=("Can't detect any of the required Python libraries "
|
for k, v in valid_at.items():
|
||||||
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
|
result['valid_at'][k] = not_before <= v <= not_after
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION,
|
|
||||||
MINIMAL_PYOPENSSL_VERSION))
|
|
||||||
|
|
||||||
if backend == 'pyopenssl':
|
|
||||||
if not PYOPENSSL_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
|
||||||
exception=PYOPENSSL_IMP_ERR)
|
|
||||||
try:
|
|
||||||
getattr(crypto.X509Req, 'get_extensions')
|
|
||||||
except AttributeError:
|
|
||||||
module.fail_json(msg='You need to have PyOpenSSL>=0.15')
|
|
||||||
|
|
||||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
|
||||||
version='2.0.0', collection_name='community.crypto')
|
|
||||||
certificate = CertificateInfoPyOpenSSL(module)
|
|
||||||
elif backend == 'cryptography':
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
|
||||||
certificate = CertificateInfoCryptography(module)
|
|
||||||
|
|
||||||
result = certificate.get_info()
|
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
except OpenSSLObjectError as exc:
|
except OpenSSLObjectError as exc:
|
||||||
module.fail_json(msg=to_native(exc))
|
module.fail_json(msg=to_native(exc))
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ certificate:
|
|||||||
|
|
||||||
import os
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||||
select_backend,
|
select_backend,
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ import traceback
|
|||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
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 (
|
from ansible_collections.community.crypto.plugins.module_utils.io import (
|
||||||
write_file,
|
write_file,
|
||||||
@@ -409,6 +409,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import
|
|||||||
identify_pem_format,
|
identify_pem_format,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import (
|
||||||
|
get_crl_info,
|
||||||
|
)
|
||||||
|
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
|
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
CRYPTOGRAPHY_IMP_ERR = None
|
||||||
@@ -550,6 +554,19 @@ class CRL(OpenSSLObject):
|
|||||||
except Exception as dummy:
|
except Exception as dummy:
|
||||||
self.crl_content = None
|
self.crl_content = None
|
||||||
self.actual_format = self.format
|
self.actual_format = self.format
|
||||||
|
data = None
|
||||||
|
|
||||||
|
self.diff_after = self.diff_before = self._get_info(data)
|
||||||
|
|
||||||
|
def _get_info(self, data):
|
||||||
|
if data is None:
|
||||||
|
return dict()
|
||||||
|
try:
|
||||||
|
result = get_crl_info(self.module, data)
|
||||||
|
result['can_parse_crl'] = True
|
||||||
|
return result
|
||||||
|
except Exception as exc:
|
||||||
|
return dict(can_parse_crl=False)
|
||||||
|
|
||||||
def remove(self):
|
def remove(self):
|
||||||
if self.backup:
|
if self.backup:
|
||||||
@@ -580,7 +597,7 @@ class CRL(OpenSSLObject):
|
|||||||
entry['invalidity_date_critical'],
|
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."""
|
"""Ensure the resource is in its desired state."""
|
||||||
|
|
||||||
state_and_perms = super(CRL, self).check(self.module, perms_required)
|
state_and_perms = super(CRL, self).check(self.module, perms_required)
|
||||||
@@ -672,15 +689,16 @@ class CRL(OpenSSLObject):
|
|||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
result = None
|
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()
|
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':
|
if self.format == 'pem':
|
||||||
result = self.crl.public_bytes(Encoding.PEM)
|
result = self.crl.public_bytes(Encoding.PEM)
|
||||||
else:
|
else:
|
||||||
result = self.crl.public_bytes(Encoding.DER)
|
result = self.crl.public_bytes(Encoding.DER)
|
||||||
|
|
||||||
if result is not None:
|
if result is not None:
|
||||||
|
self.diff_after = self._get_info(result)
|
||||||
if self.return_content:
|
if self.return_content:
|
||||||
if self.format == 'pem':
|
if self.format == 'pem':
|
||||||
self.crl_content = result
|
self.crl_content = result
|
||||||
@@ -692,7 +710,9 @@ class CRL(OpenSSLObject):
|
|||||||
self.changed = True
|
self.changed = True
|
||||||
|
|
||||||
file_args = self.module.load_file_common_arguments(self.module.params)
|
file_args = self.module.load_file_common_arguments(self.module.params)
|
||||||
if self.module.set_fs_attributes_if_different(file_args, False):
|
if self.module.check_file_absent_if_check_mode(file_args['path']):
|
||||||
|
self.changed = True
|
||||||
|
elif self.module.set_fs_attributes_if_different(file_args, False):
|
||||||
self.changed = True
|
self.changed = True
|
||||||
|
|
||||||
def dump(self, check_mode=False):
|
def dump(self, check_mode=False):
|
||||||
@@ -742,6 +762,10 @@ class CRL(OpenSSLObject):
|
|||||||
if self.return_content:
|
if self.return_content:
|
||||||
result['crl'] = self.crl_content
|
result['crl'] = self.crl_content
|
||||||
|
|
||||||
|
result['diff'] = dict(
|
||||||
|
before=self.diff_before,
|
||||||
|
after=self.diff_after,
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -810,7 +834,7 @@ def main():
|
|||||||
if module.params['state'] == 'present':
|
if module.params['state'] == 'present':
|
||||||
if module.check_mode:
|
if module.check_mode:
|
||||||
result = crl.dump(check_mode=True)
|
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)
|
module.exit_json(**result)
|
||||||
|
|
||||||
crl.generate()
|
crl.generate()
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ options:
|
|||||||
- Content of the X.509 CRL in PEM format, or Base64-encoded X.509 CRL.
|
- Content of the X.509 CRL in PEM format, or Base64-encoded X.509 CRL.
|
||||||
- Either I(path) or I(content) must be specified, but not both.
|
- Either I(path) or I(content) must be specified, but not both.
|
||||||
type: str
|
type: str
|
||||||
|
list_revoked_certificates:
|
||||||
|
description:
|
||||||
|
- If set to C(false), the list of revoked certificates is not included in the result.
|
||||||
|
- This is useful when retrieving information on large CRL files. Enumerating all revoked
|
||||||
|
certificates can take some time, including serializing the result as JSON, sending it to
|
||||||
|
the Ansible controller, and decoding it again.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
version_added: 1.7.0
|
||||||
|
|
||||||
notes:
|
notes:
|
||||||
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
|
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
|
||||||
@@ -48,6 +57,12 @@ EXAMPLES = r'''
|
|||||||
- name: Print the information
|
- name: Print the information
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg: "{{ result }}"
|
msg: "{{ result }}"
|
||||||
|
|
||||||
|
- name: Get information on CRL without list of revoked certificates
|
||||||
|
community.crypto.x509_crl_info:
|
||||||
|
path: /etc/ssl/very-large.crl
|
||||||
|
list_revoked_certificates: false
|
||||||
|
register: result
|
||||||
'''
|
'''
|
||||||
|
|
||||||
RETURN = r'''
|
RETURN = r'''
|
||||||
@@ -87,7 +102,7 @@ digest:
|
|||||||
sample: sha256WithRSAEncryption
|
sample: sha256WithRSAEncryption
|
||||||
revoked_certificates:
|
revoked_certificates:
|
||||||
description: List of certificates to be revoked.
|
description: List of certificates to be revoked.
|
||||||
returned: success
|
returned: success if I(list_revoked_certificates=true)
|
||||||
type: list
|
type: list
|
||||||
elements: dict
|
elements: dict
|
||||||
contains:
|
contains:
|
||||||
@@ -134,129 +149,22 @@ revoked_certificates:
|
|||||||
|
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import traceback
|
import binascii
|
||||||
|
|
||||||
from distutils.version import LooseVersion
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
from ansible.module_utils.common.text.converters import to_native
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
||||||
from ansible.module_utils._text import to_native
|
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
|
||||||
OpenSSLObjectError,
|
OpenSSLObjectError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
|
||||||
OpenSSLObject,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
|
||||||
cryptography_oid_to_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import (
|
|
||||||
TIMESTAMP_FORMAT,
|
|
||||||
cryptography_decode_revoked_certificate,
|
|
||||||
cryptography_dump_revoked,
|
|
||||||
cryptography_get_signature_algorithm_oid_from_crl,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
|
||||||
identify_pem_format,
|
identify_pem_format,
|
||||||
)
|
)
|
||||||
|
|
||||||
# crypto_utils
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import (
|
||||||
|
get_crl_info,
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
|
)
|
||||||
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
|
||||||
try:
|
|
||||||
import cryptography
|
|
||||||
from cryptography import x509
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
|
||||||
CRYPTOGRAPHY_FOUND = False
|
|
||||||
else:
|
|
||||||
CRYPTOGRAPHY_FOUND = True
|
|
||||||
|
|
||||||
|
|
||||||
class CRLError(OpenSSLObjectError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CRLInfo(OpenSSLObject):
|
|
||||||
"""The main module implementation."""
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
super(CRLInfo, self).__init__(
|
|
||||||
module.params['path'] or '',
|
|
||||||
'present',
|
|
||||||
False,
|
|
||||||
module.check_mode
|
|
||||||
)
|
|
||||||
|
|
||||||
self.content = module.params['content']
|
|
||||||
|
|
||||||
self.module = module
|
|
||||||
|
|
||||||
self.crl = None
|
|
||||||
if self.content is None:
|
|
||||||
try:
|
|
||||||
with open(self.path, 'rb') as f:
|
|
||||||
data = f.read()
|
|
||||||
except Exception as e:
|
|
||||||
self.module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e))
|
|
||||||
else:
|
|
||||||
data = self.content.encode('utf-8')
|
|
||||||
if not identify_pem_format(data):
|
|
||||||
data = base64.b64decode(self.content)
|
|
||||||
|
|
||||||
self.crl_pem = identify_pem_format(data)
|
|
||||||
try:
|
|
||||||
if self.crl_pem:
|
|
||||||
self.crl = x509.load_pem_x509_crl(data, default_backend())
|
|
||||||
else:
|
|
||||||
self.crl = x509.load_der_x509_crl(data, default_backend())
|
|
||||||
except Exception as e:
|
|
||||||
self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e))
|
|
||||||
|
|
||||||
def get_info(self):
|
|
||||||
result = {
|
|
||||||
'changed': False,
|
|
||||||
'format': 'pem' if self.crl_pem else 'der',
|
|
||||||
'last_update': None,
|
|
||||||
'next_update': None,
|
|
||||||
'digest': None,
|
|
||||||
'issuer_ordered': None,
|
|
||||||
'issuer': None,
|
|
||||||
'revoked_certificates': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
|
|
||||||
result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
|
|
||||||
result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl))
|
|
||||||
issuer = []
|
|
||||||
for attribute in self.crl.issuer:
|
|
||||||
issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value])
|
|
||||||
result['issuer_ordered'] = issuer
|
|
||||||
result['issuer'] = {}
|
|
||||||
for k, v in issuer:
|
|
||||||
result['issuer'][k] = v
|
|
||||||
result['revoked_certificates'] = []
|
|
||||||
for cert in self.crl:
|
|
||||||
entry = cryptography_decode_revoked_certificate(cert)
|
|
||||||
result['revoked_certificates'].append(cryptography_dump_revoked(entry))
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
# Empty method because OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
# Empty method because OpenSSLObject wants this
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -264,6 +172,7 @@ def main():
|
|||||||
argument_spec=dict(
|
argument_spec=dict(
|
||||||
path=dict(type='path'),
|
path=dict(type='path'),
|
||||||
content=dict(type='str'),
|
content=dict(type='str'),
|
||||||
|
list_revoked_certificates=dict(type='bool', default=True),
|
||||||
),
|
),
|
||||||
required_one_of=(
|
required_one_of=(
|
||||||
['path', 'content'],
|
['path', 'content'],
|
||||||
@@ -274,13 +183,22 @@ def main():
|
|||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
if module.params['content'] is None:
|
||||||
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
|
try:
|
||||||
exception=CRYPTOGRAPHY_IMP_ERR)
|
with open(module.params['path'], 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
except (IOError, OSError) as e:
|
||||||
|
module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e))
|
||||||
|
else:
|
||||||
|
data = module.params['content'].encode('utf-8')
|
||||||
|
if not identify_pem_format(data):
|
||||||
|
try:
|
||||||
|
data = base64.b64decode(module.params['content'])
|
||||||
|
except (binascii.Error, TypeError) as e:
|
||||||
|
module.fail_json(msg='Error while Base64 decoding content: {0}'.format(e))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
crl = CRLInfo(module)
|
result = get_crl_info(module, data, list_revoked_certificates=module.params['list_revoked_certificates'])
|
||||||
result = crl.get_info()
|
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
except OpenSSLObjectError as e:
|
except OpenSSLObjectError as e:
|
||||||
module.fail_json(msg=to_native(e))
|
module.fail_json(msg=to_native(e))
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ from ansible.module_utils.six import (
|
|||||||
string_types,
|
string_types,
|
||||||
text_type,
|
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
|
from ansible.plugins.action import ActionBase
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
5
tests/config.yml
Normal file
5
tests/config.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
# See template for more information:
|
||||||
|
# https://github.com/ansible/ansible/blob/devel/test/lib/ansible_test/config/config.yml
|
||||||
|
modules:
|
||||||
|
python_requires: default
|
||||||
@@ -1,2 +1,14 @@
|
|||||||
shippable/cloud/group1
|
shippable/cloud/group1
|
||||||
cloud/acme
|
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:
|
dependencies:
|
||||||
- setup_acme
|
- setup_acme
|
||||||
|
- setup_remote_tmp_dir
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
- block:
|
- block:
|
||||||
- name: Generate account keys
|
- name: Generate account keys
|
||||||
openssl_privatekey:
|
openssl_privatekey:
|
||||||
path: "{{ output_dir }}/{{ item.name }}.pem"
|
path: "{{ remote_tmp_dir }}/{{ item.name }}.pem"
|
||||||
passphrase: "{{ item.pass | default(omit, true) }}"
|
passphrase: "{{ item.pass | default(omit) | default(omit, true) }}"
|
||||||
cipher: "{{ 'auto' if item.pass | default() else omit }}"
|
cipher: "{{ 'auto' if (item.pass | default(false)) else omit }}"
|
||||||
type: ECC
|
type: ECC
|
||||||
curve: secp256r1
|
curve: secp256r1
|
||||||
force: true
|
force: true
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
|
|
||||||
- name: Parse account keys (to ease debugging some test failures)
|
- name: Parse account keys (to ease debugging some test failures)
|
||||||
openssl_privatekey_info:
|
openssl_privatekey_info:
|
||||||
path: "{{ output_dir }}/{{ item.name }}.pem"
|
path: "{{ remote_tmp_dir }}/{{ item.name }}.pem"
|
||||||
passphrase: "{{ item.pass | default(omit, true) }}"
|
passphrase: "{{ item.pass | default(omit) | default(omit, true) }}"
|
||||||
return_private_key_data: true
|
return_private_key_data: true
|
||||||
loop: "{{ account_keys }}"
|
loop: "{{ account_keys }}"
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
- name: Do not try to create account
|
- name: Do not try to create account
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
- name: Create it now (check mode, diff)
|
- name: Create it now (check mode, diff)
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
- name: Create it now
|
- name: Create it now
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
- name: Create it now (idempotent)
|
- name: Create it now (idempotent)
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -81,10 +81,15 @@
|
|||||||
- mailto:example@example.org
|
- mailto:example@example.org
|
||||||
register: account_created_idempotent
|
register: account_created_idempotent
|
||||||
|
|
||||||
|
- name: Read account key
|
||||||
|
slurp:
|
||||||
|
src: '{{ remote_tmp_dir }}/accountkey.pem'
|
||||||
|
register: slurp
|
||||||
|
|
||||||
- name: Change email address (check mode, diff)
|
- name: Change email address (check mode, diff)
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -99,7 +104,7 @@
|
|||||||
- name: Change email address
|
- name: Change email address
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -112,7 +117,7 @@
|
|||||||
- name: Change email address (idempotent)
|
- name: Change email address (idempotent)
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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 }}"
|
account_uri: "{{ account_created.account_uri }}"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
@@ -126,7 +131,7 @@
|
|||||||
- name: Cannot access account with wrong URI
|
- name: Cannot access account with wrong URI
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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' }}"
|
account_uri: "{{ account_created.account_uri ~ '12345thisdoesnotexist' }}"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
@@ -139,7 +144,7 @@
|
|||||||
- name: Clear contact email addresses (check mode, diff)
|
- name: Clear contact email addresses (check mode, diff)
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -153,7 +158,7 @@
|
|||||||
- name: Clear contact email addresses
|
- name: Clear contact email addresses
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -165,7 +170,7 @@
|
|||||||
- name: Clear contact email addresses (idempotent)
|
- name: Clear contact email addresses (idempotent)
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -177,11 +182,11 @@
|
|||||||
- name: Change account key (check mode, diff)
|
- name: Change account key (check mode, diff)
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
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 }}"
|
new_account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||||
state: changed_key
|
state: changed_key
|
||||||
contact:
|
contact:
|
||||||
@@ -193,11 +198,11 @@
|
|||||||
- name: Change account key
|
- name: Change account key
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
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 }}"
|
new_account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||||
state: changed_key
|
state: changed_key
|
||||||
contact:
|
contact:
|
||||||
@@ -207,7 +212,7 @@
|
|||||||
- name: Deactivate account (check mode, diff)
|
- name: Deactivate account (check mode, diff)
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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 }}"
|
account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
@@ -220,7 +225,7 @@
|
|||||||
- name: Deactivate account
|
- name: Deactivate account
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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 }}"
|
account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
@@ -231,7 +236,7 @@
|
|||||||
- name: Deactivate account (idempotent)
|
- name: Deactivate account (idempotent)
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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 }}"
|
account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
@@ -242,7 +247,7 @@
|
|||||||
- name: Do not try to create account II
|
- name: Do not try to create account II
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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 }}"
|
account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
@@ -255,7 +260,7 @@
|
|||||||
- name: Do not try to create account III
|
- name: Do not try to create account III
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -267,7 +272,7 @@
|
|||||||
- name: Create account with External Account Binding
|
- name: Create account with External Account Binding
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
|
|
||||||
- name: Remove output directory
|
- name: Remove output directory
|
||||||
file:
|
file:
|
||||||
path: "{{ output_dir }}"
|
path: "{{ remote_tmp_dir }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
- name: Re-create output directory
|
- name: Re-create output directory
|
||||||
file:
|
file:
|
||||||
path: "{{ output_dir }}"
|
path: "{{ remote_tmp_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
|
|
||||||
- block:
|
- block:
|
||||||
|
|||||||
@@ -1,2 +1,14 @@
|
|||||||
shippable/cloud/group1
|
shippable/cloud/group1
|
||||||
cloud/acme
|
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:
|
dependencies:
|
||||||
- setup_acme
|
- setup_acme
|
||||||
|
- setup_remote_tmp_dir
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
- block:
|
- block:
|
||||||
- name: Generate account keys
|
- name: Generate account keys
|
||||||
openssl_privatekey:
|
openssl_privatekey:
|
||||||
path: "{{ output_dir }}/{{ item }}.pem"
|
path: "{{ remote_tmp_dir }}/{{ item }}.pem"
|
||||||
type: ECC
|
type: ECC
|
||||||
curve: secp256r1
|
curve: secp256r1
|
||||||
force: true
|
force: true
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
- name: Parse account keys (to ease debugging some test failures)
|
- name: Parse account keys (to ease debugging some test failures)
|
||||||
openssl_privatekey_info:
|
openssl_privatekey_info:
|
||||||
path: "{{ output_dir }}/{{ item }}.pem"
|
path: "{{ remote_tmp_dir }}/{{ item }}.pem"
|
||||||
return_private_key_data: true
|
return_private_key_data: true
|
||||||
loop: "{{ account_keys }}"
|
loop: "{{ account_keys }}"
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
- name: Check that account does not exist
|
- name: Check that account does not exist
|
||||||
acme_account_info:
|
acme_account_info:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
- name: Create it now
|
- name: Create it now
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -44,16 +44,21 @@
|
|||||||
- name: Check that account exists
|
- name: Check that account exists
|
||||||
acme_account_info:
|
acme_account_info:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
register: account_created
|
register: account_created
|
||||||
|
|
||||||
|
- name: Read account key
|
||||||
|
slurp:
|
||||||
|
src: '{{ remote_tmp_dir }}/accountkey.pem'
|
||||||
|
register: slurp
|
||||||
|
|
||||||
- name: Clear email address
|
- name: Clear email address
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -64,7 +69,7 @@
|
|||||||
- name: Check that account was modified
|
- name: Check that account was modified
|
||||||
acme_account_info:
|
acme_account_info:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -74,7 +79,7 @@
|
|||||||
- name: Check with wrong account URI
|
- name: Check with wrong account URI
|
||||||
acme_account_info:
|
acme_account_info:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -84,7 +89,7 @@
|
|||||||
- name: Check with wrong account key
|
- name: Check with wrong account key
|
||||||
acme_account_info:
|
acme_account_info:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
|
|
||||||
- name: Remove output directory
|
- name: Remove output directory
|
||||||
file:
|
file:
|
||||||
path: "{{ output_dir }}"
|
path: "{{ remote_tmp_dir }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
- name: Re-create output directory
|
- name: Re-create output directory
|
||||||
file:
|
file:
|
||||||
path: "{{ output_dir }}"
|
path: "{{ remote_tmp_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
|
|
||||||
- block:
|
- block:
|
||||||
|
|||||||
@@ -1,2 +1,14 @@
|
|||||||
shippable/cloud/group1
|
shippable/cloud/group1
|
||||||
cloud/acme
|
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:
|
dependencies:
|
||||||
- setup_acme
|
- setup_acme
|
||||||
|
- setup_pyopenssl # needed for Ubuntu 16.04
|
||||||
|
- setup_remote_tmp_dir
|
||||||
|
- prepare_jinja2_compat
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
- block:
|
- block:
|
||||||
- name: Generate account keys
|
- name: Generate account keys
|
||||||
openssl_privatekey:
|
openssl_privatekey:
|
||||||
path: "{{ output_dir }}/{{ item.name }}.pem"
|
path: "{{ remote_tmp_dir }}/{{ item.name }}.pem"
|
||||||
type: "{{ item.type }}"
|
type: "{{ item.type }}"
|
||||||
size: "{{ item.size | default(omit) }}"
|
size: "{{ item.size | default(omit) }}"
|
||||||
curve: "{{ item.curve | default(omit) }}"
|
curve: "{{ item.curve | default(omit) }}"
|
||||||
@@ -28,15 +28,19 @@
|
|||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
|
||||||
state: absent
|
state: absent
|
||||||
|
- name: Read account key (EC384)
|
||||||
|
slurp:
|
||||||
|
src: '{{ remote_tmp_dir }}/account-ec384.pem'
|
||||||
|
register: slurp
|
||||||
- name: Create ECC384 account
|
- name: Create ECC384 account
|
||||||
acme_account:
|
acme_account:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec384.pem') }}"
|
account_key_content: "{{ slurp.content | b64decode }}"
|
||||||
state: present
|
state: present
|
||||||
allow_creation: yes
|
allow_creation: yes
|
||||||
terms_agreed: yes
|
terms_agreed: yes
|
||||||
@@ -49,7 +53,7 @@
|
|||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
account_key_src: "{{ output_dir }}/account-rsa.pem"
|
account_key_src: "{{ remote_tmp_dir }}/account-rsa.pem"
|
||||||
state: present
|
state: present
|
||||||
allow_creation: yes
|
allow_creation: yes
|
||||||
terms_agreed: yes
|
terms_agreed: yes
|
||||||
@@ -115,6 +119,10 @@
|
|||||||
set_fact:
|
set_fact:
|
||||||
cert_2_obtain_results: "{{ certificate_obtain_result }}"
|
cert_2_obtain_results: "{{ certificate_obtain_result }}"
|
||||||
cert_2_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
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
|
- name: Obtain cert 3
|
||||||
include_tasks: obtain-cert.yml
|
include_tasks: obtain-cert.yml
|
||||||
vars:
|
vars:
|
||||||
@@ -123,7 +131,7 @@
|
|||||||
key_type: ec384
|
key_type: ec384
|
||||||
subject_alt_name: "DNS:*.example.com,DNS:example.org,DNS:t1.example.com"
|
subject_alt_name: "DNS:*.example.com,DNS:example.org,DNS:t1.example.com"
|
||||||
subject_alt_name_critical: no
|
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
|
challenge: dns-01
|
||||||
modify_account: no
|
modify_account: no
|
||||||
deactivate_authzs: no
|
deactivate_authzs: no
|
||||||
@@ -231,6 +239,10 @@
|
|||||||
set_fact:
|
set_fact:
|
||||||
cert_5_recreate_2: "{{ challenge_data is changed }}"
|
cert_5_recreate_2: "{{ challenge_data is changed }}"
|
||||||
cert_5c_obtain_results: "{{ certificate_obtain_result }}"
|
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)
|
- name: Obtain cert 5 (should again by force)
|
||||||
include_tasks: obtain-cert.yml
|
include_tasks: obtain-cert.yml
|
||||||
vars:
|
vars:
|
||||||
@@ -239,7 +251,7 @@
|
|||||||
key_type: ec521
|
key_type: ec521
|
||||||
subject_alt_name: "DNS:t2.example.com"
|
subject_alt_name: "DNS:t2.example.com"
|
||||||
subject_alt_name_critical: no
|
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
|
challenge: http-01
|
||||||
modify_account: no
|
modify_account: no
|
||||||
deactivate_authzs: yes
|
deactivate_authzs: yes
|
||||||
@@ -252,189 +264,204 @@
|
|||||||
set_fact:
|
set_fact:
|
||||||
cert_5_recreate_3: "{{ challenge_data is changed }}"
|
cert_5_recreate_3: "{{ challenge_data is changed }}"
|
||||||
cert_5d_obtain_results: "{{ certificate_obtain_result }}"
|
cert_5d_obtain_results: "{{ certificate_obtain_result }}"
|
||||||
- name: Obtain cert 6
|
- block:
|
||||||
include_tasks: obtain-cert.yml
|
- name: Obtain cert 6
|
||||||
vars:
|
include_tasks: obtain-cert.yml
|
||||||
certgen_title: Certificate 6
|
vars:
|
||||||
certificate_name: cert-6
|
certgen_title: Certificate 6
|
||||||
key_type: rsa
|
certificate_name: cert-6
|
||||||
rsa_bits: "{{ default_rsa_key_size }}"
|
key_type: rsa
|
||||||
subject_alt_name: "DNS:example.org"
|
rsa_bits: "{{ default_rsa_key_size }}"
|
||||||
subject_alt_name_critical: no
|
subject_alt_name: "DNS:example.org"
|
||||||
account_key: account-ec256
|
subject_alt_name_critical: no
|
||||||
challenge: tls-alpn-01
|
account_key: account-ec256
|
||||||
modify_account: yes
|
challenge: tls-alpn-01
|
||||||
deactivate_authzs: no
|
modify_account: yes
|
||||||
force: no
|
deactivate_authzs: no
|
||||||
remaining_days: 10
|
force: no
|
||||||
terms_agreed: yes
|
remaining_days: 10
|
||||||
account_email: "example@example.org"
|
terms_agreed: yes
|
||||||
acme_expected_root_number: 0
|
account_email: "example@example.org"
|
||||||
select_chain:
|
acme_expected_root_number: 0
|
||||||
# All intermediates have the same subject key identifier, so always
|
select_chain:
|
||||||
# the first chain will be found, and we need a second condition to
|
# All intermediates have the same subject key identifier, so always
|
||||||
# make sure that the first condition actually works. (The second
|
# the first chain will be found, and we need a second condition to
|
||||||
# condition has been tested above.)
|
# make sure that the first condition actually works. (The second
|
||||||
- test_certificates: first
|
# condition has been tested above.)
|
||||||
subject_key_identifier: "{{ acme_intermediates[0].subject_key_identifier }}"
|
- test_certificates: first
|
||||||
- test_certificates: last
|
subject_key_identifier: "{{ acme_intermediates[0].subject_key_identifier }}"
|
||||||
issuer: "{{ acme_roots[1].subject }}"
|
- test_certificates: last
|
||||||
use_csr_content: true
|
issuer: "{{ acme_roots[1].subject }}"
|
||||||
- name: Store obtain results for cert 6
|
use_csr_content: true
|
||||||
set_fact:
|
- name: Store obtain results for cert 6
|
||||||
cert_6_obtain_results: "{{ certificate_obtain_result }}"
|
set_fact:
|
||||||
cert_6_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
cert_6_obtain_results: "{{ certificate_obtain_result }}"
|
||||||
- name: Obtain cert 7
|
cert_6_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
||||||
include_tasks: obtain-cert.yml
|
when: acme_intermediates[0].subject_key_identifier is defined
|
||||||
vars:
|
- block:
|
||||||
certgen_title: Certificate 7
|
- name: Obtain cert 7
|
||||||
certificate_name: cert-7
|
include_tasks: obtain-cert.yml
|
||||||
key_type: rsa
|
vars:
|
||||||
rsa_bits: "{{ default_rsa_key_size }}"
|
certgen_title: Certificate 7
|
||||||
subject_alt_name:
|
certificate_name: cert-7
|
||||||
- "IP:127.0.0.1"
|
key_type: rsa
|
||||||
# - "IP:::1"
|
rsa_bits: "{{ default_rsa_key_size }}"
|
||||||
subject_alt_name_critical: no
|
subject_alt_name:
|
||||||
account_key: account-ec256
|
- "IP:127.0.0.1"
|
||||||
challenge: http-01
|
# - "IP:::1"
|
||||||
modify_account: yes
|
subject_alt_name_critical: no
|
||||||
deactivate_authzs: no
|
account_key: account-ec256
|
||||||
force: no
|
challenge: http-01
|
||||||
remaining_days: 10
|
modify_account: yes
|
||||||
terms_agreed: yes
|
deactivate_authzs: no
|
||||||
account_email: "example@example.org"
|
force: no
|
||||||
acme_expected_root_number: 2
|
remaining_days: 10
|
||||||
select_chain:
|
terms_agreed: yes
|
||||||
- test_certificates: last
|
account_email: "example@example.org"
|
||||||
authority_key_identifier: "{{ acme_roots[2].subject_key_identifier }}"
|
acme_expected_root_number: 2
|
||||||
use_csr_content: false
|
select_chain:
|
||||||
- name: Store obtain results for cert 7
|
- test_certificates: last
|
||||||
set_fact:
|
authority_key_identifier: "{{ acme_roots[2].subject_key_identifier }}"
|
||||||
cert_7_obtain_results: "{{ certificate_obtain_result }}"
|
use_csr_content: false
|
||||||
cert_7_alternate: "{{ 2 if select_crypto_backend == 'cryptography' else 0 }}"
|
- name: Store obtain results for cert 7
|
||||||
- name: Obtain cert 8
|
set_fact:
|
||||||
include_tasks: obtain-cert.yml
|
cert_7_obtain_results: "{{ certificate_obtain_result }}"
|
||||||
vars:
|
cert_7_alternate: "{{ 2 if select_crypto_backend == 'cryptography' else 0 }}"
|
||||||
certgen_title: Certificate 8
|
when: acme_roots[2].subject_key_identifier is defined
|
||||||
certificate_name: cert-8
|
- block:
|
||||||
key_type: rsa
|
- name: Obtain cert 8
|
||||||
rsa_bits: "{{ default_rsa_key_size }}"
|
include_tasks: obtain-cert.yml
|
||||||
subject_alt_name:
|
vars:
|
||||||
- "IP:127.0.0.1"
|
certgen_title: Certificate 8
|
||||||
# IPv4 only since our test validation server doesn't work
|
certificate_name: cert-8
|
||||||
# with IPv6 (thanks to Python's socketserver).
|
key_type: rsa
|
||||||
subject_alt_name_critical: no
|
rsa_bits: "{{ default_rsa_key_size }}"
|
||||||
account_key: account-ec256
|
subject_alt_name:
|
||||||
challenge: tls-alpn-01
|
- "IP:127.0.0.1"
|
||||||
challenge_alpn_tls: acme_challenge_cert_helper
|
# IPv4 only since our test validation server doesn't work
|
||||||
modify_account: yes
|
# with IPv6 (thanks to Python's socketserver).
|
||||||
deactivate_authzs: no
|
subject_alt_name_critical: no
|
||||||
force: no
|
account_key: account-ec256
|
||||||
remaining_days: 10
|
challenge: tls-alpn-01
|
||||||
terms_agreed: yes
|
challenge_alpn_tls: acme_challenge_cert_helper
|
||||||
account_email: "example@example.org"
|
modify_account: yes
|
||||||
use_csr_content: true
|
deactivate_authzs: no
|
||||||
- name: Store obtain results for cert 8
|
force: no
|
||||||
set_fact:
|
remaining_days: 10
|
||||||
cert_8_obtain_results: "{{ certificate_obtain_result }}"
|
terms_agreed: yes
|
||||||
cert_8_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
|
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 #######################################################################
|
## DISSECT CERTIFICATES #######################################################################
|
||||||
# Make sure certificates are valid. Root certificate for Pebble equals the chain certificate.
|
# Make sure certificates are valid. Root certificate for Pebble equals the chain certificate.
|
||||||
- name: Verifying cert 1
|
- 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
|
ignore_errors: yes
|
||||||
register: cert_1_valid
|
register: cert_1_valid
|
||||||
- name: Verifying cert 2
|
- 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
|
ignore_errors: yes
|
||||||
register: cert_2_valid
|
register: cert_2_valid
|
||||||
- name: Verifying cert 3
|
- 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
|
ignore_errors: yes
|
||||||
register: cert_3_valid
|
register: cert_3_valid
|
||||||
- name: Verifying cert 4
|
- 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
|
ignore_errors: yes
|
||||||
register: cert_4_valid
|
register: cert_4_valid
|
||||||
- name: Verifying cert 5
|
- 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
|
ignore_errors: yes
|
||||||
register: cert_5_valid
|
register: cert_5_valid
|
||||||
- name: Verifying cert 6
|
- 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
|
ignore_errors: yes
|
||||||
register: cert_6_valid
|
register: cert_6_valid
|
||||||
|
when: acme_intermediates[0].subject_key_identifier is defined
|
||||||
- name: Verifying cert 7
|
- 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
|
ignore_errors: yes
|
||||||
register: cert_7_valid
|
register: cert_7_valid
|
||||||
|
when: acme_roots[2].subject_key_identifier is defined
|
||||||
- name: Verifying cert 8
|
- 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
|
ignore_errors: yes
|
||||||
register: cert_8_valid
|
register: cert_8_valid
|
||||||
|
when: cryptography_version.stdout is version('1.3', '>=')
|
||||||
# Dump certificate info
|
# Dump certificate info
|
||||||
- name: Dumping cert 1
|
- 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
|
register: cert_1_text
|
||||||
- name: Dumping cert 2
|
- 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
|
register: cert_2_text
|
||||||
- name: Dumping cert 3
|
- 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
|
register: cert_3_text
|
||||||
- name: Dumping cert 4
|
- 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
|
register: cert_4_text
|
||||||
- name: Dumping cert 5
|
- 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
|
register: cert_5_text
|
||||||
- name: Dumping cert 6
|
- 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
|
register: cert_6_text
|
||||||
|
when: acme_intermediates[0].subject_key_identifier is defined
|
||||||
- name: Dumping cert 7
|
- 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
|
register: cert_7_text
|
||||||
|
when: acme_roots[2].subject_key_identifier is defined
|
||||||
- name: Dumping cert 8
|
- 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
|
register: cert_8_text
|
||||||
|
when: cryptography_version.stdout is version('1.3', '>=')
|
||||||
# Dump certificate info
|
# Dump certificate info
|
||||||
- name: Dumping cert 1
|
- name: Dumping cert 1
|
||||||
x509_certificate_info:
|
x509_certificate_info:
|
||||||
path: "{{ output_dir }}/cert-1.pem"
|
path: "{{ remote_tmp_dir }}/cert-1.pem"
|
||||||
register: cert_1_info
|
register: cert_1_info
|
||||||
- name: Dumping cert 2
|
- name: Dumping cert 2
|
||||||
x509_certificate_info:
|
x509_certificate_info:
|
||||||
path: "{{ output_dir }}/cert-2.pem"
|
path: "{{ remote_tmp_dir }}/cert-2.pem"
|
||||||
register: cert_2_info
|
register: cert_2_info
|
||||||
- name: Dumping cert 3
|
- name: Dumping cert 3
|
||||||
x509_certificate_info:
|
x509_certificate_info:
|
||||||
path: "{{ output_dir }}/cert-3.pem"
|
path: "{{ remote_tmp_dir }}/cert-3.pem"
|
||||||
register: cert_3_info
|
register: cert_3_info
|
||||||
- name: Dumping cert 4
|
- name: Dumping cert 4
|
||||||
x509_certificate_info:
|
x509_certificate_info:
|
||||||
path: "{{ output_dir }}/cert-4.pem"
|
path: "{{ remote_tmp_dir }}/cert-4.pem"
|
||||||
register: cert_4_info
|
register: cert_4_info
|
||||||
- name: Dumping cert 5
|
- name: Dumping cert 5
|
||||||
x509_certificate_info:
|
x509_certificate_info:
|
||||||
path: "{{ output_dir }}/cert-5.pem"
|
path: "{{ remote_tmp_dir }}/cert-5.pem"
|
||||||
register: cert_5_info
|
register: cert_5_info
|
||||||
- name: Dumping cert 6
|
- name: Dumping cert 6
|
||||||
x509_certificate_info:
|
x509_certificate_info:
|
||||||
path: "{{ output_dir }}/cert-6.pem"
|
path: "{{ remote_tmp_dir }}/cert-6.pem"
|
||||||
register: cert_6_info
|
register: cert_6_info
|
||||||
|
when: acme_intermediates[0].subject_key_identifier is defined
|
||||||
- name: Dumping cert 7
|
- name: Dumping cert 7
|
||||||
x509_certificate_info:
|
x509_certificate_info:
|
||||||
path: "{{ output_dir }}/cert-7.pem"
|
path: "{{ remote_tmp_dir }}/cert-7.pem"
|
||||||
register: cert_7_info
|
register: cert_7_info
|
||||||
|
when: acme_roots[2].subject_key_identifier is defined
|
||||||
- name: Dumping cert 8
|
- name: Dumping cert 8
|
||||||
x509_certificate_info:
|
x509_certificate_info:
|
||||||
path: "{{ output_dir }}/cert-8.pem"
|
path: "{{ remote_tmp_dir }}/cert-8.pem"
|
||||||
register: cert_8_info
|
register: cert_8_info
|
||||||
|
when: cryptography_version.stdout is version('1.3', '>=')
|
||||||
## GET ACCOUNT ORDERS #########################################################################
|
## GET ACCOUNT ORDERS #########################################################################
|
||||||
- name: Don't retrieve orders
|
- name: Don't retrieve orders
|
||||||
acme_account_info:
|
acme_account_info:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -443,7 +470,7 @@
|
|||||||
- name: Retrieve orders as URL list (1/2)
|
- name: Retrieve orders as URL list (1/2)
|
||||||
acme_account_info:
|
acme_account_info:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -452,7 +479,7 @@
|
|||||||
- name: Retrieve orders as URL list (2/2)
|
- name: Retrieve orders as URL list (2/2)
|
||||||
acme_account_info:
|
acme_account_info:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -461,7 +488,7 @@
|
|||||||
- name: Retrieve orders as object list (1/2)
|
- name: Retrieve orders as object list (1/2)
|
||||||
acme_account_info:
|
acme_account_info:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -470,7 +497,7 @@
|
|||||||
- name: Retrieve orders as object list (2/2)
|
- name: Retrieve orders as object list (2/2)
|
||||||
acme_account_info:
|
acme_account_info:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
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_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
|
|||||||
@@ -8,38 +8,48 @@
|
|||||||
- name: Obtain root and intermediate certificates
|
- name: Obtain root and intermediate certificates
|
||||||
get_url:
|
get_url:
|
||||||
url: "http://{{ acme_host }}:5000/{{ item.0 }}-certificate-for-ca/{{ item.1 }}"
|
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) }}"
|
loop: "{{ query('nested', types, root_numbers) }}"
|
||||||
|
|
||||||
- name: Analyze root certificates
|
- name: Analyze root certificates
|
||||||
x509_certificate_info:
|
x509_certificate_info:
|
||||||
path: "{{ output_dir }}/acme-root-{{ item }}.pem"
|
path: "{{ remote_tmp_dir }}/acme-root-{{ item }}.pem"
|
||||||
loop: "{{ root_numbers }}"
|
loop: "{{ root_numbers }}"
|
||||||
register: acme_roots
|
register: acme_roots
|
||||||
|
|
||||||
- name: Analyze intermediate certificates
|
- name: Analyze intermediate certificates
|
||||||
x509_certificate_info:
|
x509_certificate_info:
|
||||||
path: "{{ output_dir }}/acme-intermediate-{{ item }}.pem"
|
path: "{{ remote_tmp_dir }}/acme-intermediate-{{ item }}.pem"
|
||||||
loop: "{{ root_numbers }}"
|
loop: "{{ root_numbers }}"
|
||||||
register: acme_intermediates
|
register: acme_intermediates
|
||||||
|
|
||||||
- set_fact:
|
- name: Read root certificates
|
||||||
x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}"
|
slurp:
|
||||||
y__: "{{ lookup('file', output_dir ~ '/acme-root-' ~ item.item ~ '.pem', rstrip=False) }}"
|
src: "{{ remote_tmp_dir ~ '/acme-root-' ~ item ~ '.pem' }}"
|
||||||
loop: "{{ acme_roots.results }}"
|
loop: "{{ root_numbers }}"
|
||||||
register: acme_roots_tmp
|
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:
|
- set_fact:
|
||||||
x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}"
|
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 }}"
|
loop: "{{ acme_intermediates.results }}"
|
||||||
register: acme_intermediates_tmp
|
register: acme_intermediates_tmp
|
||||||
|
|
||||||
- set_fact:
|
- set_fact:
|
||||||
acme_roots: "{{ acme_roots_tmp.results | map(attribute='ansible_facts.x__') | list }}"
|
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_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:
|
vars:
|
||||||
types:
|
types:
|
||||||
@@ -88,12 +98,12 @@
|
|||||||
|
|
||||||
- name: Remove output directory
|
- name: Remove output directory
|
||||||
file:
|
file:
|
||||||
path: "{{ output_dir }}"
|
path: "{{ remote_tmp_dir }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
- name: Re-create output directory
|
- name: Re-create output directory
|
||||||
file:
|
file:
|
||||||
path: "{{ output_dir }}"
|
path: "{{ remote_tmp_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
|
|
||||||
- block:
|
- block:
|
||||||
|
|||||||
@@ -7,6 +7,14 @@
|
|||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- "'DNS:example.com' in cert_1_text.stdout"
|
- "'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
|
- name: Check that certificate 1 retrieval got all chains
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
@@ -15,9 +23,9 @@
|
|||||||
- "'cert' in cert_1_obtain_results.all_chains[cert_1_alternate | int]"
|
- "'cert' in cert_1_obtain_results.all_chains[cert_1_alternate | int]"
|
||||||
- "'chain' 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]"
|
- "'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"
|
- "(slurp.results[0].content | b64decode) == 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"
|
- "(slurp.results[1].content | b64decode) == 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[2].content | b64decode) == cert_1_obtain_results.all_chains[cert_1_alternate | int].full_chain"
|
||||||
|
|
||||||
- name: Check that certificate 2 is valid
|
- name: Check that certificate 2 is valid
|
||||||
assert:
|
assert:
|
||||||
@@ -28,6 +36,14 @@
|
|||||||
that:
|
that:
|
||||||
- "'DNS:*.example.com' in cert_2_text.stdout"
|
- "'DNS:*.example.com' in cert_2_text.stdout"
|
||||||
- "'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
|
- name: Check that certificate 1 retrieval got all chains
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
@@ -36,9 +52,9 @@
|
|||||||
- "'cert' in cert_2_obtain_results.all_chains[cert_2_alternate | int]"
|
- "'cert' in cert_2_obtain_results.all_chains[cert_2_alternate | int]"
|
||||||
- "'chain' 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]"
|
- "'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"
|
- "(slurp.results[0].content | b64decode) == 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"
|
- "(slurp.results[1].content | b64decode) == 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[2].content | b64decode) == cert_2_obtain_results.all_chains[cert_2_alternate | int].full_chain"
|
||||||
|
|
||||||
- name: Check that certificate 3 is valid
|
- name: Check that certificate 3 is valid
|
||||||
assert:
|
assert:
|
||||||
@@ -50,6 +66,14 @@
|
|||||||
- "'DNS:*.example.com' in cert_3_text.stdout"
|
- "'DNS:*.example.com' in cert_3_text.stdout"
|
||||||
- "'DNS:example.org' in cert_3_text.stdout"
|
- "'DNS:example.org' in cert_3_text.stdout"
|
||||||
- "'DNS:t1.example.com' 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
|
- name: Check that certificate 1 retrieval got all chains
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
@@ -58,9 +82,9 @@
|
|||||||
- "'cert' in cert_3_obtain_results.all_chains[cert_3_alternate | int]"
|
- "'cert' in cert_3_obtain_results.all_chains[cert_3_alternate | int]"
|
||||||
- "'chain' 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]"
|
- "'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"
|
- "(slurp.results[0].content | b64decode) == 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"
|
- "(slurp.results[1].content | b64decode) == 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[2].content | b64decode) == cert_3_obtain_results.all_chains[cert_3_alternate | int].full_chain"
|
||||||
|
|
||||||
- name: Check that certificate 4 is valid
|
- name: Check that certificate 4 is valid
|
||||||
assert:
|
assert:
|
||||||
@@ -100,14 +124,38 @@
|
|||||||
that:
|
that:
|
||||||
- cert_5_recreate_3 == True
|
- cert_5_recreate_3 == True
|
||||||
|
|
||||||
- name: Check that certificate 6 is valid
|
- block:
|
||||||
assert:
|
- name: Check that certificate 6 is valid
|
||||||
that:
|
assert:
|
||||||
- cert_6_valid is not failed
|
that:
|
||||||
- name: Check that certificate 6 contains correct SANs
|
- cert_6_valid is not failed
|
||||||
assert:
|
- name: Check that certificate 6 contains correct SANs
|
||||||
that:
|
assert:
|
||||||
- "'DNS:example.org' in cert_6_text.stdout"
|
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
|
- name: Validate that orders were not retrieved
|
||||||
assert:
|
assert:
|
||||||
|
|||||||
@@ -1,2 +1,14 @@
|
|||||||
shippable/cloud/group1
|
shippable/cloud/group1
|
||||||
cloud/acme
|
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:
|
dependencies:
|
||||||
- setup_acme
|
- setup_acme
|
||||||
|
- setup_remote_tmp_dir
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
- block:
|
- block:
|
||||||
- name: Generate account keys
|
- name: Generate account keys
|
||||||
openssl_privatekey:
|
openssl_privatekey:
|
||||||
path: "{{ output_dir }}/{{ item.name }}.pem"
|
path: "{{ remote_tmp_dir }}/{{ item.name }}.pem"
|
||||||
type: "{{ item.type }}"
|
type: "{{ item.type }}"
|
||||||
size: "{{ item.size | default(omit) }}"
|
size: "{{ item.size | default(omit) }}"
|
||||||
curve: "{{ item.curve | default(omit) }}"
|
curve: "{{ item.curve | default(omit) }}"
|
||||||
@@ -22,6 +22,10 @@
|
|||||||
type: RSA
|
type: RSA
|
||||||
size: "{{ default_rsa_key_size }}"
|
size: "{{ default_rsa_key_size }}"
|
||||||
## CREATE ACCOUNTS AND OBTAIN CERTIFICATES ####################################################
|
## CREATE ACCOUNTS AND OBTAIN CERTIFICATES ####################################################
|
||||||
|
- name: Read account key (EC256)
|
||||||
|
slurp:
|
||||||
|
src: '{{ remote_tmp_dir }}/account-ec256.pem'
|
||||||
|
register: slurp_account_key
|
||||||
- name: Obtain cert 1
|
- name: Obtain cert 1
|
||||||
include_tasks: obtain-cert.yml
|
include_tasks: obtain-cert.yml
|
||||||
vars:
|
vars:
|
||||||
@@ -31,7 +35,7 @@
|
|||||||
rsa_bits: "{{ default_rsa_key_size }}"
|
rsa_bits: "{{ default_rsa_key_size }}"
|
||||||
subject_alt_name: "DNS:example.com"
|
subject_alt_name: "DNS:example.com"
|
||||||
subject_alt_name_critical: no
|
subject_alt_name_critical: no
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-ec256.pem') }}"
|
account_key_content: "{{ slurp_account_key.content | b64decode }}"
|
||||||
challenge: http-01
|
challenge: http-01
|
||||||
modify_account: yes
|
modify_account: yes
|
||||||
deactivate_authzs: no
|
deactivate_authzs: no
|
||||||
@@ -76,8 +80,8 @@
|
|||||||
- name: Revoke certificate 1 via account key
|
- name: Revoke certificate 1 via account key
|
||||||
acme_certificate_revoke:
|
acme_certificate_revoke:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
account_key_src: "{{ output_dir }}/account-ec256.pem"
|
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
|
||||||
certificate: "{{ output_dir }}/cert-1.pem"
|
certificate: "{{ remote_tmp_dir }}/cert-1.pem"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
@@ -86,19 +90,23 @@
|
|||||||
- name: Revoke certificate 2 via certificate private key
|
- name: Revoke certificate 2 via certificate private key
|
||||||
acme_certificate_revoke:
|
acme_certificate_revoke:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
private_key_src: "{{ output_dir }}/cert-2.key"
|
private_key_src: "{{ remote_tmp_dir }}/cert-2.key"
|
||||||
private_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
private_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
|
||||||
certificate: "{{ output_dir }}/cert-2.pem"
|
certificate: "{{ remote_tmp_dir }}/cert-2.pem"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
ignore_errors: yes
|
ignore_errors: yes
|
||||||
register: cert_2_revoke
|
register: cert_2_revoke
|
||||||
|
- name: Read account key (RSA)
|
||||||
|
slurp:
|
||||||
|
src: '{{ remote_tmp_dir }}/account-rsa.pem'
|
||||||
|
register: slurp_account_key
|
||||||
- name: Revoke certificate 3 via account key (fullchain)
|
- name: Revoke certificate 3 via account key (fullchain)
|
||||||
acme_certificate_revoke:
|
acme_certificate_revoke:
|
||||||
select_crypto_backend: "{{ select_crypto_backend }}"
|
select_crypto_backend: "{{ select_crypto_backend }}"
|
||||||
account_key_content: "{{ lookup('file', output_dir ~ '/account-rsa.pem') }}"
|
account_key_content: "{{ slurp_account_key.content | b64decode }}"
|
||||||
certificate: "{{ output_dir }}/cert-3-fullchain.pem"
|
certificate: "{{ remote_tmp_dir }}/cert-3-fullchain.pem"
|
||||||
acme_version: 2
|
acme_version: 2
|
||||||
acme_directory: https://{{ acme_host }}:14000/dir
|
acme_directory: https://{{ acme_host }}:14000/dir
|
||||||
validate_certs: no
|
validate_certs: no
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
|
|
||||||
- name: Remove output directory
|
- name: Remove output directory
|
||||||
file:
|
file:
|
||||||
path: "{{ output_dir }}"
|
path: "{{ remote_tmp_dir }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
- name: Re-create output directory
|
- name: Re-create output directory
|
||||||
file:
|
file:
|
||||||
path: "{{ output_dir }}"
|
path: "{{ remote_tmp_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
|
|
||||||
- block:
|
- block:
|
||||||
|
|||||||
@@ -1,2 +1,14 @@
|
|||||||
shippable/cloud/group1
|
shippable/cloud/group1
|
||||||
cloud/acme
|
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
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user