Compare commits

...

64 Commits

Author SHA1 Message Date
Felix Fontein
ded8568802 Release 2.26.4. 2025-07-26 14:37:51 +02:00
Felix Fontein
e145fe71a9 Move EE tests to nox. (#941) (#942)
(cherry picked from commit 0636123f56)
2025-07-25 20:44:16 +02:00
Felix Fontein
b6887ab1f4 Improve error message when lodaing corrupt private key or private key with wrong passphrase. (#939) (#940)
(cherry picked from commit f219cac94c)
2025-07-25 15:08:45 +00:00
Felix Fontein
71e9d2273a Prepare 2.26.4. 2025-07-25 14:41:18 +02:00
patchback[bot]
0f2f5a5fe9 Replace FreeBSD 13.3 with 13.5. (#937) (#938)
(cherry picked from commit b4303b3a32)

Co-authored-by: Felix Fontein <felix@fontein.de>
2025-07-24 21:58:15 +02:00
patchback[bot]
d0099b4f3e Ensure consistent SSH key format with idempotent Ed25519 key regeneration (#932) (#933)
* Ensure consistent SSH key format with idempotent Ed25519 key regeneration

* Update plugins/modules/openssh_keypair.py



* removed extra whitespace

---------


(cherry picked from commit b2ab04861e)

Co-authored-by: Aditya Putta <puttaa@yahoo.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
2025-07-11 12:35:16 +02:00
patchback[bot]
b1dfcf89a4 Docs: mention RFC 9773 instead of the ARI draft (#929) (#930)
* Mention RFC 9773 instead of the ARI draft.

* Remove mentions of the draft.

(cherry picked from commit fcb50ed142)

Co-authored-by: Felix Fontein <felix@fontein.de>
2025-07-06 17:24:28 +02:00
Felix Fontein
e200d363f2 Change devel to 2.19. (#926) 2025-07-01 21:34:01 +02:00
patchback[bot]
513c2fd5a0 [PR #921/bd070e85 backport][stable-2] Docs: use :ansplugin: (#922)
* Use :ansplugin:. (#921)

(cherry picked from commit bd070e85a3)

* Add ignore.txt entries.

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
2025-06-25 22:52:26 +02:00
Felix Fontein
63d347e9f2 Add YAML lint config for extra docs.
(cherry picked from commits d4fa1d094a
and 087aa70fe9)
2025-06-17 17:47:47 +02:00
Felix Fontein
5eccff6190 Next release will be 2.26.4. 2025-06-14 17:05:06 +02:00
Felix Fontein
5ca4ecb54b Release 2.26.3. 2025-06-14 16:44:49 +02:00
Felix Fontein
ea970a044f Stick to community.general 10.x.y for CI. 2025-06-13 06:11:49 +02:00
Felix Fontein
3e3318f059 acme_account: check for 'externalAccountRequired' error (#919) (#920)
* Check for 'externalAccountRequired' error.

* Add changelog fragment.

(cherry picked from commit 056ae1cf69)
2025-06-13 06:10:41 +02:00
Felix Fontein
ae6fb88896 Prepare 2.26.3. 2025-06-12 22:45:19 +02:00
patchback[bot]
66d7989222 Add HARICA to the list of tested CAs (#915) (#916)
* Add HARICA to the list of tested CAs



* Add ZeroSSL to list.

---------



(cherry picked from commit ec063d8515)

Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
Co-authored-by: Daniel Ziegenberg <daniel@ziegenberg.at>
Co-authored-by: Felix Fontein <felix@fontein.de>
2025-06-08 21:08:04 +02:00
Felix Fontein
99d6a17653 Fix some ansible-lint issues (#907) (#908)
* Fix fqcn[action-core].

* Fix fqcn[action].

* Fix jinja[spacing].

(cherry picked from commit 8792635bef)
2025-05-30 22:43:43 +02:00
patchback[bot]
edeed24e8f Document supported curves for Elliptic Curve keys on ACME Accounts (#904) (#906)
(cherry picked from commit 7241d5543a)

Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
Co-authored-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2025-05-30 13:08:08 +02:00
Felix Fontein
2f3809c84b Next release will be 2.26.3. 2025-05-22 22:02:19 +02:00
Felix Fontein
4f92a02bc4 Release 2.26.2. 2025-05-22 21:19:40 +02:00
Felix Fontein
f7b01bae60 Prepare 2.26.2. 2025-05-22 19:58:28 +02:00
Felix Fontein
43d7868646 [stable-2] Remove entrust announcement (#901)
* Announce removal of Entrust content from community.crypto 3.0.0.

* Add more information on Entrust removal.
2025-05-22 19:57:08 +02:00
patchback[bot]
3fbf173674 Add RHEL 10.0 to CI. (#899) (#902)
(cherry picked from commit 41b71bb60c)

Co-authored-by: Felix Fontein <felix@fontein.de>
2025-05-22 06:43:36 +02:00
Felix Fontein
d350b94ae6 Lint doc fragments.
(cherry picked from commit ef230011fd)
2025-05-01 16:48:13 +02:00
Felix Fontein
a75cc7345a Fix typo.
(cherry picked from commit 718021b714)
2025-04-29 08:13:56 +02:00
Felix Fontein
f7795f65b0 Remove 'upcoming' information on 2.0.0. 2025-04-28 12:06:34 +02:00
Felix Fontein
b5d3277798 The next release will be 2.26.2.
There will be (very likely) no more minor releases from this branch.
2025-04-28 11:59:23 +02:00
Felix Fontein
f1a170d427 This is now the stable-2 branch. 2025-04-28 11:58:55 +02:00
Felix Fontein
278dcc5dda Release 2.26.1. 2025-04-28 11:51:52 +02:00
Felix Fontein
805771d2ed Add reformat commit to .git-blame-ignore-revs. 2025-04-28 10:49:05 +02:00
Felix Fontein
5ab56c431f Add ignore.txt entries. 2025-04-28 10:48:00 +02:00
Felix Fontein
aec1826c34 Reformat everything with black.
I had to undo the u string prefix removals to not drop Python 2 compatibility.
That's why black isn't enabled in antsibull-nox.toml yet.
2025-04-28 10:48:00 +02:00
Felix Fontein
04a0d38e3b Do not supply passphrase when killing keyslot. (#868) 2025-04-27 22:19:12 +02:00
Felix Fontein
aa9e7b6dfb Add isort and flake8 to CI (#869)
* Run isort.

* Clean up unused assignments.

* Add flake8 linting step.
2025-04-27 22:18:29 +02:00
Felix Fontein
ac134ee5f5 Prepare 2.26.1. 2025-04-27 12:37:24 +02:00
Felix Fontein
154f3c6cd7 Add no_log=False to passphrase_encoding. 2025-04-26 14:12:19 +02:00
Felix Fontein
594ece1a70 Add reformat commit to .git-blame-ignore-revs. 2025-04-26 12:22:32 +02:00
Felix Fontein
33ef158b09 Fix linting errors. 2025-04-26 12:18:21 +02:00
Felix Fontein
51a4f76f26 Add yamllint to antsibull-nox and add config files, and prepare ignore.txt entries. 2025-04-26 12:18:21 +02:00
Felix Fontein
f04f0c883e Reformat noxfile.py. 2025-04-25 07:17:16 +02:00
Felix Fontein
72d04577df Add REUSE badge. Fix info on blanket license statement for changelog fragments. 2025-04-24 22:45:38 +02:00
Felix Fontein
194ab4694e Make reuse conformant (#509)
* Revert "Improve reuse test."

This reverts commit 7eddfda7f8.

* Revert "Update README."

This reverts commit b0ec28c6a1.

* Revert "Add exceptions."

This reverts commit c749421292.
(This commit got adjusted to changes in community.crypto.)

* Revert "Revert "Add .license file for vendored third-party certificates.""

This reverts commit 034b900a30.

* Remove no longer necessary REUSE workflow.

This is now checked by nox.

* Fix filenames.

* Update .gitignore.
2025-04-24 22:43:06 +02:00
Felix Fontein
04967efe26 Replace vendored certificates with self-created certificates of similar structure (#862)
* Create script to reproduce certs.

* Recreate the certificates and update the tests.

* Anonymize certificates.

* Make mostly reproducable by storing the private keys.

I've tried to hide the private keys so that 'security checkers' won't find them
and won't complain. Let's see whether that works...
2025-04-24 22:31:01 +02:00
Felix Fontein
dbff2a69e2 Remove FreeBSD 14.0 from CI. (#863)
In ansible-core it has been replaced with 14.1, but we're already testing against that.
Ref: 3546111f2d
2025-04-22 19:02:36 +02:00
Felix Fontein
d8773697de Adjust times. 2025-04-19 19:56:34 +02:00
Felix Fontein
046aeab5e2 Run extra sanity tests with nox. (#861) 2025-04-19 17:54:14 +02:00
Felix Fontein
a9d6e0048c Work around bug in ansible-core that censors mailto URIs. (#859) 2025-04-10 12:58:33 +02:00
Felix Fontein
a2d821f960 Migrate .reuse/dep5 to REUSE.toml. 2025-03-29 12:17:19 +01:00
Giorgos Drosos
b1451b3460 Skip openssh_cert test on Rocky Linux 9+ due to SHA-1 restrictions (#856)
* Make openssh_cert second algorithm tests compatible with Rocky

* Fix typo

* Merge conditions

Co-authored-by: Felix Fontein <felix@fontein.de>

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
2025-03-25 20:30:28 +01:00
Felix Fontein
8dabbd8f94 Use shared unit test utils from community.internal_test_tools (#854)
* Use shared unit test utils from community.internal_test_tools.

* Make sure community.internal_test_tools is installed in CI.
2025-03-12 22:12:12 +01:00
Felix Fontein
a1669d490f CI: Remove usage of ubuntu-20.04, add FreeBSD 13.5 (#853)
* Switch from ubuntu-20.04 to ubuntu-latest for old Ansible versions.

* [TEMP] Change to trigger full CI.

* Add FreeBSD 13.5.
2025-03-12 07:24:02 +01:00
Felix Fontein
9ac42ffb11 The next expected release will be 2.27.0. 2025-03-11 20:35:14 +01:00
Felix Fontein
e58fe63dde Release 2.26.0. 2025-03-11 20:09:02 +01:00
Felix Fontein
0d1f260328 Prepare 2.26.0. 2025-03-10 21:53:06 +01:00
Florian Apolloner
ba55ba7381 openssl_pkcs12: Add support for certificate_content and other_certificates_content (#848)
* openssl_pkcs12: Add support for `certificate_content` and `other_certificates_content`

Co-authored-by: Felix Fontein <felix@fontein.de>

* Added minimal tests.

The tests are minimal because internally it always ends up with the
_content variants, so even when supplying a file most of the internal
code paths then use the content.

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
2025-03-10 21:44:31 +01:00
Felix Fontein
260bdb1572 Improve tests (#852)
* Use no longer needed wrapper.

* Improve tests.
2025-03-08 10:48:34 +01:00
Felix Fontein
7d5ebad188 Group CI updates. 2025-03-03 19:00:29 +01:00
Felix Fontein
78d9fe5813 Add macOS 15.3. (#849) 2025-02-26 20:45:08 +01:00
Felix Fontein
a42e541326 Cleanup AZP config similarly to ansible-core did some years ago. (#846) 2025-02-10 22:52:24 +01:00
Felix Fontein
673b18d9a9 The next expected release will be 2.26.0. 2025-02-09 19:56:25 +01:00
Felix Fontein
2a99218162 Release 2.25.0. 2025-02-09 19:29:41 +01:00
Felix Fontein
e1763e22ae Prepare 2.25.0 release. 2025-02-09 14:25:42 +01:00
ilia-kats
2433fdab98 luks_device: allow passphrases to contain newlines (#844)
* luks_device: allow passphrases to contain newlines

This is useful when passing binary keyfiles from an ansible vault, as
it removes the restriction that the binary data cannot contain newlines.
The only exception is adding a new key to an existing container, as in
that case the two passphrases are separated by a new line.

* add integration tests and a changelog fragment

* attempt to also make luks_add_key work with passphrases containing
newlines

* use a deterministic method to generate keyfile 3, improve changelog
formatting

* add licence and copyright to keyfile3.txt to satisfy CI
2025-02-09 14:24:16 +01:00
Felix Fontein
cb6edf1a5f The next expected release will be 2.25.0. 2025-01-19 13:28:11 +01:00
374 changed files with 19997 additions and 18095 deletions

View File

@@ -36,8 +36,6 @@ variables:
value: ansible_collections/community/crypto value: ansible_collections/community/crypto
- name: coverageBranches - name: coverageBranches
value: main value: main
- name: pipelinesCoverage
value: coverage
- name: entryPoint - name: entryPoint
value: tests/utils/shippable/shippable.sh value: tests/utils/shippable/shippable.sh
- name: fetchDepth - name: fetchDepth
@@ -52,19 +50,17 @@ pool: Standard
stages: stages:
### Sanity & units ### Sanity & units
- stage: Ansible_devel - stage: Ansible_2_19
displayName: Sanity & Units devel displayName: Sanity & Units 2.19
dependsOn: [] dependsOn: []
jobs: jobs:
- template: templates/matrix.yml - template: templates/matrix.yml
parameters: parameters:
targets: targets:
- name: Sanity - name: Sanity
test: 'devel/sanity/1' test: '2.19/sanity/1'
- name: Sanity Extra # Only on devel
test: 'devel/sanity/extra'
- name: Units - name: Units
test: 'devel/units/1' test: '2.19/units/1'
- stage: Ansible_2_18 - stage: Ansible_2_18
displayName: Sanity & Units 2.18 displayName: Sanity & Units 2.18
dependsOn: [] dependsOn: []
@@ -99,13 +95,13 @@ stages:
- name: Units - name: Units
test: '2.16/units/1' test: '2.16/units/1'
### Docker ### Docker
- stage: Docker_devel - stage: Docker_2_19
displayName: Docker devel displayName: Docker 2.19
dependsOn: [] dependsOn: []
jobs: jobs:
- template: templates/matrix.yml - template: templates/matrix.yml
parameters: parameters:
testFormat: devel/linux/{0} testFormat: 2.19/linux/{0}
targets: targets:
- name: Fedora 41 - name: Fedora 41
test: fedora41 test: fedora41
@@ -169,13 +165,13 @@ stages:
- 2 - 2
### Community Docker ### Community Docker
- stage: Docker_community_devel - stage: Docker_community_2_19
displayName: Docker (community images) devel displayName: Docker (community images) 2.19
dependsOn: [] dependsOn: []
jobs: jobs:
- template: templates/matrix.yml - template: templates/matrix.yml
parameters: parameters:
testFormat: devel/linux-community/{0} testFormat: 2.19/linux-community/{0}
targets: targets:
- name: Debian Bullseye - name: Debian Bullseye
test: debian-bullseye/3.9 test: debian-bullseye/3.9
@@ -188,13 +184,13 @@ stages:
- 2 - 2
### Remote ### Remote
- stage: Remote_devel_extra_vms - stage: Remote_2_19_extra_vms
displayName: Remote devel extra VMs displayName: Remote 2.19 extra VMs
dependsOn: [] dependsOn: []
jobs: jobs:
- template: templates/matrix.yml - template: templates/matrix.yml
parameters: parameters:
testFormat: devel/{0} testFormat: 2.19/{0}
targets: targets:
- name: Alpine 3.21 - name: Alpine 3.21
test: alpine/3.21 test: alpine/3.21
@@ -206,22 +202,24 @@ stages:
test: ubuntu/24.04 test: ubuntu/24.04
groups: groups:
- vm - vm
- stage: Remote_devel - stage: Remote_2_19
displayName: Remote devel displayName: Remote 2.19
dependsOn: [] dependsOn: []
jobs: jobs:
- template: templates/matrix.yml - template: templates/matrix.yml
parameters: parameters:
testFormat: devel/{0} testFormat: 2.19/{0}
targets: targets:
- name: macOS 14.3 - name: macOS 15.3
test: macos/14.3 test: macos/15.3
- name: RHEL 10.0
test: rhel/10.0
- name: RHEL 9.5 - name: RHEL 9.5
test: rhel/9.5 test: rhel/9.5
- name: FreeBSD 14.2 - name: FreeBSD 14.2
test: freebsd/14.2 test: freebsd/14.2
- name: FreeBSD 13.4 - name: FreeBSD 13.5
test: freebsd/13.4 test: freebsd/13.5
groups: groups:
- 1 - 1
- 2 - 2
@@ -233,6 +231,8 @@ stages:
parameters: parameters:
testFormat: 2.18/{0} testFormat: 2.18/{0}
targets: targets:
- name: macOS 14.3
test: macos/14.3
- name: RHEL 9.4 - name: RHEL 9.4
test: rhel/9.4 test: rhel/9.4
- name: FreeBSD 14.1 - name: FreeBSD 14.1
@@ -250,10 +250,8 @@ stages:
targets: targets:
- name: RHEL 9.3 - name: RHEL 9.3
test: rhel/9.3 test: rhel/9.3
- name: FreeBSD 13.3 - name: FreeBSD 13.5
test: freebsd/13.3 test: freebsd/13.5
- name: FreeBSD 14.0
test: freebsd/14.0
groups: groups:
- 1 - 1
- 2 - 2
@@ -279,14 +277,14 @@ stages:
- 1 - 1
- 2 - 2
### Generic ### Generic
- stage: Generic_devel - stage: Generic_2_19
displayName: Generic devel displayName: Generic 2.19
dependsOn: [] dependsOn: []
jobs: jobs:
- template: templates/matrix.yml - template: templates/matrix.yml
parameters: parameters:
nameFormat: Python {0} nameFormat: Python {0}
testFormat: devel/generic/{0} testFormat: 2.19/generic/{0}
targets: targets:
- test: "3.8" - test: "3.8"
# - test: "3.9" # - test: "3.9"
@@ -345,21 +343,21 @@ stages:
- stage: Summary - stage: Summary
condition: succeededOrFailed() condition: succeededOrFailed()
dependsOn: dependsOn:
- Ansible_devel - Ansible_2_19
- Ansible_2_18 - Ansible_2_18
- Ansible_2_17 - Ansible_2_17
- Ansible_2_16 - Ansible_2_16
- Remote_devel_extra_vms - Remote_2_19_extra_vms
- Remote_devel - Remote_2_19
- Remote_2_18 - Remote_2_18
- Remote_2_17 - Remote_2_17
- Remote_2_16 - Remote_2_16
- Docker_devel - Docker_2_19
- Docker_2_18 - Docker_2_18
- Docker_2_17 - Docker_2_17
- Docker_2_16 - Docker_2_16
- Docker_community_devel - Docker_community_2_19
- Generic_devel - Generic_2_19
- Generic_2_18 - Generic_2_18
- Generic_2_17 - Generic_2_17
- Generic_2_16 - Generic_2_16

View File

@@ -28,16 +28,6 @@ jobs:
- bash: .azure-pipelines/scripts/report-coverage.sh - bash: .azure-pipelines/scripts/report-coverage.sh
displayName: Generate Coverage Report displayName: Generate Coverage Report
condition: gt(variables.coverageFileCount, 0) condition: gt(variables.coverageFileCount, 0)
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: Cobertura
# Azure Pipelines only accepts a single coverage data file.
# That means only Python or PowerShell coverage can be uploaded, but not both.
# Set the "pipelinesCoverage" variable to determine which type is uploaded.
# Use "coverage" for Python and "coverage-powershell" for PowerShell.
summaryFileLocation: "$(outputPath)/reports/$(pipelinesCoverage).xml"
displayName: Publish to Azure Pipelines
condition: gt(variables.coverageFileCount, 0)
- bash: .azure-pipelines/scripts/publish-codecov.py "$(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)

8
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,8 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# Reformat YAML: https://github.com/ansible-collections/community.crypto/pull/866
33ef158b094f16d5e04ea9db3ed8bad010744d02
# Reformat with black, keeping Python 2 compatibility: https://github.com/ansible-collections/community.crypto/pull/871
aec1826c34051b9e7f8af7950489915b661e320b

View File

@@ -9,3 +9,7 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
groups:
ci:
patterns:
- "*"

View File

@@ -7,7 +7,7 @@
# https://github.com/marketplace/actions/ansible-test # https://github.com/marketplace/actions/ansible-test
name: EOL CI name: EOL CI
on: 'on':
# Run EOL CI against all pushes (direct commits, also merged PRs), Pull Requests # Run EOL CI against all pushes (direct commits, also merged PRs), Pull Requests
push: push:
branches: branches:
@@ -36,15 +36,7 @@ jobs:
- '2.13' - '2.13'
- '2.14' - '2.14'
- '2.15' - '2.15'
# Ansible-test on various stable branches does not yet work well with cgroups v2. runs-on: ubuntu-latest
# Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04
# image for these stable branches. The list of branches where this is necessary will
# shrink over time, check out https://github.com/ansible-collections/news-for-maintainers/issues/28
# for the latest list.
runs-on: >-
${{ contains(fromJson(
'["2.9", "2.10", "2.11"]'
), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }}
steps: steps:
- name: Perform sanity testing - name: Perform sanity testing
uses: felixfontein/ansible-test-gh-action@main uses: felixfontein/ansible-test-gh-action@main
@@ -53,19 +45,13 @@ jobs:
ansible-core-version: stable-${{ matrix.ansible }} ansible-core-version: stable-${{ matrix.ansible }}
codecov-token: ${{ secrets.CODECOV_TOKEN }} codecov-token: ${{ secrets.CODECOV_TOKEN }}
coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }} coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }}
pre-test-cmd: >-
git clone --depth=1 --single-branch https://github.com/ansible-collections/community.internal_test_tools.git ../../community/internal_test_tools
pull-request-change-detection: 'true' pull-request-change-detection: 'true'
testing-type: sanity testing-type: sanity
units: units:
# Ansible-test on various stable branches does not yet work well with cgroups v2. runs-on: ubuntu-latest
# Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04
# image for these stable branches. The list of branches where this is necessary will
# shrink over time, check out https://github.com/ansible-collections/news-for-maintainers/issues/28
# for the latest list.
runs-on: >-
${{ contains(fromJson(
'["2.9", "2.10", "2.11"]'
), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }}
name: EOL Units (Ⓐ${{ matrix.ansible }}) name: EOL Units (Ⓐ${{ matrix.ansible }})
strategy: strategy:
# As soon as the first unit test fails, cancel the others to free up the CI queue # As soon as the first unit test fails, cancel the others to free up the CI queue
@@ -90,19 +76,13 @@ jobs:
ansible-core-version: stable-${{ matrix.ansible }} ansible-core-version: stable-${{ matrix.ansible }}
codecov-token: ${{ secrets.CODECOV_TOKEN }} codecov-token: ${{ secrets.CODECOV_TOKEN }}
coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }} coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }}
pre-test-cmd: >-
git clone --depth=1 --single-branch https://github.com/ansible-collections/community.internal_test_tools.git ../../community/internal_test_tools
pull-request-change-detection: 'true' pull-request-change-detection: 'true'
testing-type: units testing-type: units
integration: integration:
# Ansible-test on various stable branches does not yet work well with cgroups v2. runs-on: ubuntu-latest
# Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04
# image for these stable branches. The list of branches where this is necessary will
# shrink over time, check out https://github.com/ansible-collections/news-for-maintainers/issues/28
# for the latest list.
runs-on: >-
${{ contains(fromJson(
'["2.9", "2.10", "2.11"]'
), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }}
name: EOL I (Ⓐ${{ matrix.ansible }}+${{ matrix.docker }}+py${{ matrix.python }}:${{ matrix.target }}) name: EOL I (Ⓐ${{ matrix.ansible }}+${{ matrix.docker }}+py${{ matrix.python }}:${{ matrix.target }})
strategy: strategy:
fail-fast: false fail-fast: false
@@ -304,7 +284,7 @@ jobs:
pre-test-cmd: >- pre-test-cmd: >-
git clone --depth=1 --single-branch https://github.com/ansible-collections/community.internal_test_tools.git ../../community/internal_test_tools git clone --depth=1 --single-branch https://github.com/ansible-collections/community.internal_test_tools.git ../../community/internal_test_tools
; ;
git clone --depth=1 --single-branch https://github.com/ansible-collections/community.general.git ../../community/general git clone --depth=1 --single-branch --branch stable-10 https://github.com/ansible-collections/community.general.git ../../community/general
pull-request-change-detection: 'true' pull-request-change-detection: 'true'
target: ${{ matrix.target }} target: ${{ matrix.target }}
target-python-version: ${{ matrix.python }} target-python-version: ${{ matrix.python }}

View File

@@ -7,7 +7,7 @@ name: Collection Docs
concurrency: concurrency:
group: docs-pr-${{ github.head_ref }} group: docs-pr-${{ github.head_ref }}
cancel-in-progress: true cancel-in-progress: true
on: 'on':
pull_request_target: pull_request_target:
types: [opened, synchronize, reopened, closed] types: [opened, synchronize, reopened, closed]

View File

@@ -7,7 +7,7 @@ name: Collection Docs
concurrency: concurrency:
group: docs-push-${{ github.sha }} group: docs-push-${{ github.sha }}
cancel-in-progress: true cancel-in-progress: true
on: 'on':
push: push:
branches: branches:
- main - main

View File

@@ -1,180 +0,0 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
name: execution environment
on:
# Run CI against all pushes (direct commits, also merged PRs), Pull Requests
push:
branches:
- main
- stable-*
pull_request:
# Run CI once per day (at 04:45 UTC)
# This ensures that even if there haven't been commits that we are still testing against latest version of ansible-builder
schedule:
- cron: '45 4 * * *'
env:
NAMESPACE: community
COLLECTION_NAME: crypto
jobs:
build:
name: Build and test EE (${{ matrix.name }})
strategy:
fail-fast: false
matrix:
name:
- ''
ansible_core:
- ''
ansible_runner:
- ''
base_image:
- ''
pre_base:
- ''
extra_vars:
- ''
other_deps:
- ''
exclude:
- ansible_core: ''
include:
- name: ansible-core devel @ RHEL UBI 9
ansible_core: https://github.com/ansible/ansible/archive/devel.tar.gz
ansible_runner: ansible-runner
other_deps: |2
python_interpreter:
package_system: python3.11 python3.11-pip python3.11-wheel python3.11-cryptography
python_path: "/usr/bin/python3.11"
base_image: docker.io/redhat/ubi9:latest
pre_base: '"#"'
# For some reason ansible-builder will not install EPEL dependencies on RHEL
extra_vars: -e has_no_pyopenssl=true
- name: ansible-core 2.15 @ Rocky Linux 9
ansible_core: https://github.com/ansible/ansible/archive/stable-2.15.tar.gz
ansible_runner: ansible-runner
base_image: quay.io/rockylinux/rockylinux:9
pre_base: RUN dnf install -y epel-release
# For some reason ansible-builder will not install EPEL dependencies on Rocky Linux
extra_vars: -e has_no_pyopenssl=true
- name: ansible-core 2.14 @ CentOS Stream 9
ansible_core: https://github.com/ansible/ansible/archive/stable-2.14.tar.gz
ansible_runner: ansible-runner
base_image: quay.io/centos/centos:stream9
pre_base: RUN dnf install -y epel-release epel-next-release
# For some reason, PyOpenSSL is **broken** on CentOS Stream 9 / EPEL
extra_vars: -e has_no_pyopenssl=true
- name: ansible-core 2.13 @ RHEL UBI 8
ansible_core: https://github.com/ansible/ansible/archive/stable-2.13.tar.gz
ansible_runner: ansible-runner
other_deps: |2
python_interpreter:
package_system: python39 python39-pip python39-wheel python39-cryptography
base_image: docker.io/redhat/ubi8:latest
pre_base: '"#"'
# We don't have PyOpenSSL for Python 3.9
extra_vars: -e has_no_pyopenssl=true
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
with:
path: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install ansible-builder and ansible-navigator
run: pip install ansible-builder ansible-navigator
- name: Verify requirements
run: ansible-builder introspect --sanitize .
- name: Make sure galaxy.yml has version entry
run: >-
python -c
'import yaml ;
f = open("galaxy.yml", "rb") ;
data = yaml.safe_load(f) ;
f.close() ;
data["version"] = data.get("version") or "0.0.1" ;
f = open("galaxy.yml", "wb") ;
f.write(yaml.dump(data).encode("utf-8")) ;
f.close() ;
'
working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
- name: Build collection
run: |
ansible-galaxy collection build --output-path ../../../
working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
- name: Create files for building execution environment
run: |
COLLECTION_FILENAME="$(ls "${NAMESPACE}-${COLLECTION_NAME}"-*.tar.gz)"
# EE config
cat > execution-environment.yml <<EOF
---
version: 3
dependencies:
ansible_core:
package_pip: ${{ matrix.ansible_core }}
ansible_runner:
package_pip: ${{ matrix.ansible_runner }}
galaxy: requirements.yml
${{ matrix.other_deps }}
images:
base_image:
name: ${{ matrix.base_image }}
additional_build_files:
- src: ${COLLECTION_FILENAME}
dest: src
additional_build_steps:
prepend_base:
- ${{ matrix.pre_base }}
EOF
echo "::group::execution-environment.yml"
cat execution-environment.yml
echo "::endgroup::"
# Requirements
cat > requirements.yml <<EOF
---
collections:
- name: src/${COLLECTION_FILENAME}
type: file
EOF
echo "::group::requirements.yml"
cat requirements.yml
echo "::endgroup::"
- name: Build image based on ${{ matrix.base_image }}
run: |
ansible-builder build --verbosity 3 --tag test-ee:latest --container-runtime podman
- name: Show images
run: podman image ls
- name: Run basic tests
run: >
ansible-navigator run
--mode stdout
--container-engine podman
--pull-policy never
--set-environment-variable ANSIBLE_PRIVATE_ROLE_VARS=true
--execution-environment-image test-ee:latest
-v
all.yml
${{ matrix.extra_vars }}
working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}/tests/ee

View File

@@ -1,20 +0,0 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
name: import-galaxy
'on':
# Run CI against all pushes (direct commits, also merged PRs) to main, and all Pull Requests
push:
branches:
- main
- stable-*
pull_request:
jobs:
import-galaxy:
permissions:
contents: read
name: Test to import built collection artifact with Galaxy importer
uses: ansible-community/github-action-test-galaxy-import/.github/workflows/test-galaxy-import.yml@main

31
.github/workflows/nox.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
name: nox
'on':
push:
branches:
- main
- stable-*
pull_request:
# Run CI once per day (at 09:00 UTC)
schedule:
- cron: '0 9 * * *'
workflow_dispatch:
jobs:
nox:
runs-on: ubuntu-latest
name: "Run extra sanity tests"
steps:
- name: Check out collection
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Run nox
uses: ansible-community/antsibull-nox@main
ansible-test:
uses: ansible-community/antsibull-nox/.github/workflows/reusable-nox-matrix.yml@main

View File

@@ -1,38 +0,0 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
name: Verify REUSE
on:
push:
branches:
- main
- stable-*
pull_request:
branches:
- main
- stable-*
# Run CI once per day (at 04:45 UTC)
schedule:
- cron: '45 4 * * *'
jobs:
check:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Remove some files before checking REUSE compliance
run: |
rm -f tests/integration/targets/*/files/*.pem
rm -f tests/integration/targets/*/files/roots/*.pem
- name: REUSE Compliance Check
uses: fsfe/reuse-action@v5

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@
# Community.crypt specific things # Community.crypt specific things
/changelogs/.plugin-cache.yaml /changelogs/.plugin-cache.yaml
/tests/integration/inventory
# Created by https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv # Created by https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv

View File

@@ -1,5 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Files: changelogs/fragments/*
Copyright: Ansible Project
License: GPL-3.0-or-later

53
.yamllint Normal file
View File

@@ -0,0 +1,53 @@
---
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2025 Felix Fontein <felix@fontein.de>
extends: default
ignore: |
/changelogs/
rules:
line-length:
max: 300
level: error
document-start:
present: true
document-end: false
truthy:
level: error
allowed-values:
- 'true'
- 'false'
indentation:
spaces: 2
indent-sequences: true
key-duplicates: enable
trailing-spaces: enable
new-line-at-end-of-file: disable
hyphens:
max-spaces-after: 1
empty-lines:
max: 2
max-start: 0
max-end: 0
commas:
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
colons:
max-spaces-before: 0
max-spaces-after: 1
brackets:
min-spaces-inside: 0
max-spaces-inside: 0
braces:
min-spaces-inside: 0
max-spaces-inside: 1
octal-values:
forbid-implicit-octal: true
forbid-explicit-octal: true
comments:
min-spaces-from-content: 1
comments-indentation: false

54
.yamllint-docs Normal file
View File

@@ -0,0 +1,54 @@
---
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2025 Felix Fontein <felix@fontein.de>
extends: default
ignore: |
/changelogs/
rules:
line-length:
max: 160
level: error
document-start:
present: false
document-end:
present: false
truthy:
level: error
allowed-values:
- 'true'
- 'false'
indentation:
spaces: 2
indent-sequences: true
key-duplicates: enable
trailing-spaces: enable
new-line-at-end-of-file: disable
hyphens:
max-spaces-after: 1
empty-lines:
max: 2
max-start: 0
max-end: 0
commas:
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
colons:
max-spaces-before: 0
max-spaces-after: 1
brackets:
min-spaces-inside: 0
max-spaces-inside: 0
braces:
min-spaces-inside: 0
max-spaces-inside: 1
octal-values:
forbid-implicit-octal: true
forbid-explicit-octal: true
comments:
min-spaces-from-content: 1
comments-indentation: false

54
.yamllint-examples Normal file
View File

@@ -0,0 +1,54 @@
---
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2025 Felix Fontein <felix@fontein.de>
extends: default
ignore: |
/changelogs/
rules:
line-length:
max: 160
level: error
document-start:
present: true
document-end:
present: false
truthy:
level: error
allowed-values:
- 'true'
- 'false'
indentation:
spaces: 2
indent-sequences: true
key-duplicates: enable
trailing-spaces: enable
new-line-at-end-of-file: disable
hyphens:
max-spaces-after: 1
empty-lines:
max: 2
max-start: 0
max-end: 0
commas:
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
colons:
max-spaces-before: 0
max-spaces-after: 1
brackets:
min-spaces-inside: 0
max-spaces-inside: 0
braces:
min-spaces-inside: 0
max-spaces-inside: 1
octal-values:
forbid-implicit-octal: true
forbid-explicit-octal: true
comments:
min-spaces-from-content: 1
comments-indentation: false

53
.yamllint-extra-docs Normal file
View File

@@ -0,0 +1,53 @@
---
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2025 Felix Fontein <felix@fontein.de>
extends: default
ignore: |
/changelogs/
rules:
line-length:
max: 160
level: error
document-start: disable
document-end:
present: false
truthy:
level: error
allowed-values:
- 'true'
- 'false'
indentation:
spaces: 2
indent-sequences: true
key-duplicates: enable
trailing-spaces: enable
new-line-at-end-of-file: disable
hyphens:
max-spaces-after: 1
empty-lines:
max: 2
max-start: 0
max-end: 0
commas:
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
colons:
max-spaces-before: 0
max-spaces-after: 1
brackets:
min-spaces-inside: 0
max-spaces-inside: 0
braces:
min-spaces-inside: 0
max-spaces-inside: 1
octal-values:
forbid-implicit-octal: true
forbid-explicit-octal: true
comments:
min-spaces-from-content: 1
comments-indentation: false

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,89 @@ Community Crypto Release Notes
.. contents:: Topics .. contents:: Topics
v2.26.4
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- Improve error message when loading a private key fails due to correct private key files or wrong passwords. Also include the original cryptography error since it likely contains more helpful information (https://github.com/ansible-collections/community.crypto/issues/936, https://github.com/ansible-collections/community.crypto/pull/939).
v2.26.3
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- acme_account - make work with CAs that do not accept any account request without External Account Binding data (https://github.com/ansible-collections/community.crypto/issues/918, https://github.com/ansible-collections/community.crypto/pull/919).
v2.26.2
=======
Release Summary
---------------
Maintenance release announcing removal of the Entrust content from community.crypto 3.0.0.
Deprecated Features
-------------------
- The Entrust service in currently being sunsetted after the sale of Entrust's Public Certificates Business to Sectigo; see `the announcement with key dates <https://www.entrust.com/tls-certificate-information-center>`__ and `the migration brief for customers <https://www.sectigo.com/uploads/resources/EOL_Migration-Brief-End-Customer.pdf>`__ for details (https://github.com/ansible-collections/community.crypto/issues/895, https://github.com/ansible-collections/community.crypto/pull/901).
- ecs_certificate - the module will be removed from community.crypto 3.0.0 (https://github.com/ansible-collections/community.crypto/issues/895, https://github.com/ansible-collections/community.crypto/pull/901).
- ecs_domain - the module will be removed from community.crypto 3.0.0 (https://github.com/ansible-collections/community.crypto/issues/895, https://github.com/ansible-collections/community.crypto/pull/901).
- x509_certificate - the ``entrust`` provider will be removed from community.crypto 3.0.0 (https://github.com/ansible-collections/community.crypto/issues/895, https://github.com/ansible-collections/community.crypto/pull/901).
- x509_certificate_pipe - the ``entrust`` provider will be removed from community.crypto 3.0.0 (https://github.com/ansible-collections/community.crypto/issues/895, https://github.com/ansible-collections/community.crypto/pull/901).
v2.26.1
=======
Release Summary
---------------
Bugfix and maintenance release with improved CI.
Bugfixes
--------
- luks_device - mark parameter ``passphrase_encoding`` as ``no_log=False`` to avoid confusing warning (https://github.com/ansible-collections/community.crypto/pull/867).
- luks_device - removing a specific keyslot with ``remove_keyslot`` caused the module to hang while cryptsetup was waiting for a passphrase from stdin, while the module did not supply one. Since a keyslot is not necessary, do not provide one (https://github.com/ansible-collections/community.crypto/issues/864, https://github.com/ansible-collections/community.crypto/pull/868).
v2.26.0
=======
Release Summary
---------------
Feature release.
Minor Changes
-------------
- openssl_pkcs12 - the module now supports ``certificate_content``/``other_certificates_content`` for cases where the data already exists in memory and not yet in a file (https://github.com/ansible-collections/community.crypto/issues/847, https://github.com/ansible-collections/community.crypto/pull/848).
v2.25.0
=======
Release Summary
---------------
Feature release.
Minor Changes
-------------
- luks_device - allow passphrases to contain newlines (https://github.com/ansible-collections/community.crypto/pull/844).
v2.24.0 v2.24.0
======= =======

View File

@@ -7,9 +7,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
# Ansible Community Crypto Collection # Ansible Community Crypto Collection
[![Documentation](https://img.shields.io/badge/docs-brightgreen.svg)](https://docs.ansible.com/ansible/devel/collections/community/crypto/) [![Documentation](https://img.shields.io/badge/docs-brightgreen.svg)](https://docs.ansible.com/ansible/devel/collections/community/crypto/)
[![Build Status](https://dev.azure.com/ansible/community.crypto/_apis/build/status/CI?branchName=main)](https://dev.azure.com/ansible/community.crypto/_build?definitionId=21) [![Build Status](https://dev.azure.com/ansible/community.crypto/_apis/build/status/CI?branchName=stable-2)](https://dev.azure.com/ansible/community.crypto/_build?definitionId=21)
[![EOL CI](https://github.com/ansible-collections/community.crypto/actions/workflows/ansible-test.yml/badge.svg?branch=main)](https://github.com/ansible-collections/community.crypto/actions) [![EOL CI](https://github.com/ansible-collections/community.crypto/actions/workflows/ansible-test.yml/badge.svg?branch=stable-2)](https://github.com/ansible-collections/community.crypto/actions)
[![Nox CI](https://github.com/ansible-collections/community.crypto/actions/workflows/nox.yml/badge.svg?branch=stable-2)](https://github.com/ansible-collections/community.crypto/actions)
[![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.crypto)](https://codecov.io/gh/ansible-collections/community.crypto) [![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.crypto)](https://codecov.io/gh/ansible-collections/community.crypto)
[![REUSE status](https://api.reuse.software/badge/github.com/ansible-collections/community.crypto)](https://api.reuse.software/info/github.com/ansible-collections/community.crypto)
Provides modules for [Ansible](https://www.ansible.com/community) for various cryptographic operations. Provides modules for [Ansible](https://www.ansible.com/community) for various cryptographic operations.
@@ -38,7 +40,7 @@ For more information about communication, see the [Ansible communication guide](
## Tested with Ansible ## Tested with Ansible
Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12, ansible-core 2.13, ansible-core 2.14, ansible-core 2.15, ansible-core 2.16, ansible-core-2.17, and ansible-core 2.18 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported. Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12, ansible-core 2.13, ansible-core 2.14, ansible-core 2.15, ansible-core 2.16, ansible-core-2.17, ansible-core-2.18, and ansible-core 2.19 releases. Ansible versions before 2.9.10 are not supported.
## External requirements ## External requirements
@@ -52,7 +54,7 @@ Browsing the [**latest** collection documentation](https://docs.ansible.com/ansi
Browsing the [**devel** collection documentation](https://docs.ansible.com/ansible/devel/collections/community/crypto) shows docs for the _latest version released on Galaxy_. Browsing the [**devel** collection documentation](https://docs.ansible.com/ansible/devel/collections/community/crypto) shows docs for the _latest version released on Galaxy_.
We also separately publish [**latest commit** collection documentation](https://ansible-collections.github.io/community.crypto/branch/main/) which shows docs for the _latest commit in the `main` branch_. We also separately publish [**latest commit** collection documentation](https://ansible-collections.github.io/community.crypto/branch/stable-2/) which shows docs for the _latest commit in the `stable-2` branch_.
If you use the Ansible package and do not update collections independently, use **latest**. If you install or update this collection directly from Galaxy, use **devel**. If you are looking to contribute, use **latest commit**. If you use the Ansible package and do not update collections independently, use **latest**. If you install or update this collection directly from Galaxy, use **devel**. If you are looking to contribute, use **latest commit**.
@@ -107,7 +109,7 @@ If you use the Ansible package and do not update collections independently, use
- luks_device module - luks_device module
- parse_serial and to_serial filters - parse_serial and to_serial filters
You can also find a list of all modules and plugins with documentation on the [Ansible docs site](https://docs.ansible.com/ansible/latest/collections/community/crypto/), or the [latest commit collection documentation](https://ansible-collections.github.io/community.crypto/branch/main/). You can also find a list of all modules and plugins with documentation on the [Ansible docs site](https://docs.ansible.com/ansible/latest/collections/community/crypto/), or the [latest commit collection documentation](https://ansible-collections.github.io/community.crypto/branch/stable-2/).
## Using this collection ## Using this collection
@@ -139,19 +141,15 @@ See [Ansible's dev guide](https://docs.ansible.com/ansible/devel/dev_guide/devel
## Release notes ## Release notes
See the [changelog](https://github.com/ansible-collections/community.crypto/blob/main/CHANGELOG.md). See the [changelog](https://github.com/ansible-collections/community.crypto/blob/stable-2/CHANGELOG.md).
## Roadmap ## Roadmap
We plan to regularly release minor and patch versions, whenever new features are added or bugs fixed. Our collection follows [semantic versioning](https://semver.org/), so breaking changes will only happen in major releases. We plan to regularly release minor and patch versions, whenever new features are added or bugs fixed. Our collection follows [semantic versioning](https://semver.org/), so breaking changes will only happen in major releases.
Most modules will drop PyOpenSSL support in version 2.0.0 of the collection, i.e. in the next major version. We currently plan to release 2.0.0 somewhen during 2021. Around then, the supported versions of the most common distributions will contain a new enough version of ``cryptography``. In 2.0.0, the following notable features have been removed:
* PyOpenSSL backends of all modules, except ``openssl_pkcs12`` which did now have a ``cryptography`` backend for a long time due to lack of support of PKCS#12 functionality in ``cryptography``. (This changed.)
Once 2.0.0 has been released, bugfixes will still be backported to 1.0.0 for some time, and some features might also be backported. If we do not want to backport something ourselves because we think it is not worth the effort, backport PRs by non-maintainers are usually accepted. * The ``assertonly`` provider of ``x509_certificate`` has been removed.
In 2.0.0, the following notable features will be removed:
* PyOpenSSL backends of all modules, except ``openssl_pkcs12`` which does not have a ``cryptography`` backend due to lack of support of PKCS#12 functionality in ``cryptography``.
* The ``assertonly`` provider of ``x509_certificate`` will be removed.
## More information ## More information
@@ -164,8 +162,8 @@ In 2.0.0, the following notable features will be removed:
This collection is primarily licensed and distributed as a whole under the GNU General Public License v3.0 or later. This collection is primarily licensed and distributed as a whole under the GNU General Public License v3.0 or later.
See [LICENSES/GPL-3.0-or-later.txt](https://github.com/ansible-collections/community.crypto/blob/main/COPYING) for the full text. See [LICENSES/GPL-3.0-or-later.txt](https://github.com/ansible-collections/community.crypto/blob/stable-2/COPYING) for the full text.
Parts of the collection are licensed under the [Apache 2.0 license](https://github.com/ansible-collections/community.crypto/blob/main/LICENSES/Apache-2.0.txt) (`plugins/module_utils/crypto/_obj2txt.py` and `plugins/module_utils/crypto/_objects_data.py`), the [BSD 2-Clause license](https://github.com/ansible-collections/community.crypto/blob/main/LICENSES/BSD-2-Clause.txt) (`plugins/module_utils/ecs/api.py`), the [BSD 3-Clause license](https://github.com/ansible-collections/community.crypto/blob/main/LICENSES/BSD-3-Clause.txt) (`plugins/module_utils/crypto/_obj2txt.py`, `tests/integration/targets/prepare_jinja2_compat/filter_plugins/jinja_compatibility.py`), and the [PSF 2.0 license](https://github.com/ansible-collections/community.crypto/blob/main/LICENSES/PSF-2.0.txt) (`plugins/module_utils/_version.py`). This only applies to vendored files in ``plugins/module_utils/`` and to the ECS module utils. Parts of the collection are licensed under the [Apache 2.0 license](https://github.com/ansible-collections/community.crypto/blob/stable-2/LICENSES/Apache-2.0.txt) (`plugins/module_utils/crypto/_obj2txt.py` and `plugins/module_utils/crypto/_objects_data.py`), the [BSD 2-Clause license](https://github.com/ansible-collections/community.crypto/blob/stable-2/LICENSES/BSD-2-Clause.txt) (`plugins/module_utils/ecs/api.py`), the [BSD 3-Clause license](https://github.com/ansible-collections/community.crypto/blob/stable-2/LICENSES/BSD-3-Clause.txt) (`plugins/module_utils/crypto/_obj2txt.py`, `tests/integration/targets/prepare_jinja2_compat/filter_plugins/jinja_compatibility.py`), and the [PSF 2.0 license](https://github.com/ansible-collections/community.crypto/blob/stable-2/LICENSES/PSF-2.0.txt) (`plugins/module_utils/_version.py`). This only applies to vendored files in ``plugins/module_utils/`` and to the ECS module utils.
Almost all files have a machine readable `SDPX-License-Identifier:` comment denoting its respective license(s) or an equivalent entry in an accompanying `.license` file. Only changelog fragments (which will not be part of a release) are covered by a blanket statement in `.reuse/dep5`. Right now a few vendored PEM files do not have licensing information as well. This conforms to the [REUSE specification](https://reuse.software/spec/) up to the aforementioned PEM files. All files have a machine readable `SDPX-License-Identifier:` comment denoting its respective license(s) or an equivalent entry in an accompanying `.license` file. Only changelog fragments (which will not be part of a release) are covered by a blanket statement in `REUSE.toml`. This conforms to the [REUSE specification](https://reuse.software/spec/).

11
REUSE.toml Normal file
View File

@@ -0,0 +1,11 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
version = 1
[[annotations]]
path = "changelogs/fragments/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "Ansible Project"
SPDX-License-Identifier = "GPL-3.0-or-later"

102
antsibull-nox.toml Normal file
View File

@@ -0,0 +1,102 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2025 Felix Fontein <felix@fontein.de>
[collection_sources]
"community.internal_test_tools" = "git+https://github.com/ansible-collections/community.internal_test_tools.git,main"
[sessions]
[sessions.lint]
run_isort = true
isort_config = "tests/nox-config-isort.cfg"
run_black = false
run_flake8 = true
flake8_config = "tests/nox-config-flake8.ini"
run_pylint = false
run_yamllint = true
yamllint_config = ".yamllint"
yamllint_config_plugins = ".yamllint-docs"
yamllint_config_plugins_examples = ".yamllint-examples"
yamllint_config_extra_docs = ".yamllint-extra-docs"
run_mypy = false
[sessions.docs_check]
validate_collection_refs="all"
[sessions.license_check]
run_reuse = true
[sessions.extra_checks]
run_no_unwanted_files = true
no_unwanted_files_module_extensions = [".py"]
no_unwanted_files_yaml_extensions = [".yml"]
run_action_groups = true
[[sessions.extra_checks.action_groups_config]]
name = "acme"
pattern = "^acme_.*$"
exclusions = [
"acme_ari_info", # does not support ACME account
"acme_certificate_renewal_info", # does not support ACME account
"acme_challenge_cert_helper", # does not support (and need) any common parameters
]
doc_fragment = "community.crypto.attributes.actiongroup_acme"
[sessions.build_import_check]
run_galaxy_importer = true
# [sessions.ansible_lint]
[[sessions.ee_check.execution_environments]]
name = "devel-ubi-9"
description = "ansible-core devel @ RHEL UBI 9"
test_playbooks = ["tests/ee/all.yml"]
config.images.base_image.name = "docker.io/redhat/ubi9:latest"
config.dependencies.ansible_core.package_pip = "https://github.com/ansible/ansible/archive/devel.tar.gz"
config.dependencies.ansible_runner.package_pip = "ansible-runner"
config.dependencies.python_interpreter.package_system = "python3.12 python3.12-pip python3.12-wheel python3.12-cryptography"
config.dependencies.python_interpreter.python_path = "/usr/bin/python3.12"
runtime_environment = {"ANSIBLE_PRIVATE_ROLE_VARS" = "true"}
# For some reason ansible-builder will not install EPEL dependencies on RHEL
runtime_extra_vars = { "has_no_pyopenssl" = "true" }
[[sessions.ee_check.execution_environments]]
name = "2.15-rocky-9"
description = "ansible-core 2.15 @ Rocky Linux 9"
test_playbooks = ["tests/ee/all.yml"]
config.images.base_image.name = "quay.io/rockylinux/rockylinux:9"
config.dependencies.ansible_core.package_pip = "https://github.com/ansible/ansible/archive/stable-2.15.tar.gz"
config.dependencies.ansible_runner.package_pip = "ansible-runner"
config.additional_build_steps.prepend_base = [
"RUN dnf install -y epel-release",
]
runtime_environment = {"ANSIBLE_PRIVATE_ROLE_VARS" = "true"}
# For some reason ansible-builder will not install EPEL dependencies on Rocky Linux
runtime_extra_vars = { "has_no_pyopenssl" = "true" }
[[sessions.ee_check.execution_environments]]
name = "2.14-centos-stream-9"
description = "ansible-core 2.14 @ CentOS Stream 9"
test_playbooks = ["tests/ee/all.yml"]
config.images.base_image.name = "quay.io/centos/centos:stream9"
config.dependencies.ansible_core.package_pip = "https://github.com/ansible/ansible/archive/stable-2.14.tar.gz"
config.dependencies.ansible_runner.package_pip = "ansible-runner"
config.additional_build_steps.prepend_base = [
"RUN dnf install -y epel-release epel-next-release",
]
runtime_environment = {"ANSIBLE_PRIVATE_ROLE_VARS" = "true"}
# For some reason, PyOpenSSL is **broken** on CentOS Stream 9 / EPEL
runtime_extra_vars = { "has_no_pyopenssl" = "true" }
[[sessions.ee_check.execution_environments]]
name = "2.13-ubi-8"
description = "ansible-core 2.13 @ RHEL UBI 8"
test_playbooks = ["tests/ee/all.yml"]
config.images.base_image.name = "docker.io/redhat/ubi8:latest"
config.dependencies.ansible_core.package_pip = "https://github.com/ansible/ansible/archive/stable-2.13.tar.gz"
config.dependencies.ansible_runner.package_pip = "ansible-runner"
config.dependencies.python_interpreter.package_system = "python39 python39-pip python39-wheel python39-cryptography"
runtime_environment = {"ANSIBLE_PRIVATE_ROLE_VARS" = "true"}
# We don't have PyOpenSSL for Python 3.9
runtime_extra_vars = { "has_no_pyopenssl" = "true" }

View File

@@ -1607,3 +1607,87 @@ releases:
name: acme_certificate_order_validate name: acme_certificate_order_validate
namespace: '' namespace: ''
release_date: '2025-01-19' release_date: '2025-01-19'
2.25.0:
changes:
minor_changes:
- luks_device - allow passphrases to contain newlines (https://github.com/ansible-collections/community.crypto/pull/844).
release_summary: Feature release.
fragments:
- 2.25.0.yml
- luks_device_passphrase_newlines.yml
release_date: '2025-02-09'
2.26.0:
changes:
minor_changes:
- openssl_pkcs12 - the module now supports ``certificate_content``/``other_certificates_content``
for cases where the data already exists in memory and not yet in a file
(https://github.com/ansible-collections/community.crypto/issues/847, https://github.com/ansible-collections/community.crypto/pull/848).
release_summary: Feature release.
fragments:
- 2.26.0.yml
- openssl_pkcs12_content.yml
release_date: '2025-03-11'
2.26.1:
changes:
bugfixes:
- luks_device - mark parameter ``passphrase_encoding`` as ``no_log=False``
to avoid confusing warning (https://github.com/ansible-collections/community.crypto/pull/867).
- luks_device - removing a specific keyslot with ``remove_keyslot`` caused
the module to hang while cryptsetup was waiting for a passphrase from stdin,
while the module did not supply one. Since a keyslot is not necessary, do
not provide one (https://github.com/ansible-collections/community.crypto/issues/864,
https://github.com/ansible-collections/community.crypto/pull/868).
release_summary: Bugfix and maintenance release with improved CI.
fragments:
- 2.26.1.yml
- 867-passphrase-encoding-nolog.yml
- 868-luks-remove-keyslot.yml
release_date: '2025-04-28'
2.26.2:
changes:
deprecated_features:
- The Entrust service in currently being sunsetted after the sale of Entrust's
Public Certificates Business to Sectigo; see `the announcement with key
dates <https://www.entrust.com/tls-certificate-information-center>`__ and
`the migration brief for customers <https://www.sectigo.com/uploads/resources/EOL_Migration-Brief-End-Customer.pdf>`__
for details (https://github.com/ansible-collections/community.crypto/issues/895,
https://github.com/ansible-collections/community.crypto/pull/901).
- ecs_certificate - the module will be removed from community.crypto 3.0.0
(https://github.com/ansible-collections/community.crypto/issues/895, https://github.com/ansible-collections/community.crypto/pull/901).
- ecs_domain - the module will be removed from community.crypto 3.0.0 (https://github.com/ansible-collections/community.crypto/issues/895,
https://github.com/ansible-collections/community.crypto/pull/901).
- x509_certificate - the ``entrust`` provider will be removed from community.crypto
3.0.0 (https://github.com/ansible-collections/community.crypto/issues/895,
https://github.com/ansible-collections/community.crypto/pull/901).
- x509_certificate_pipe - the ``entrust`` provider will be removed from community.crypto
3.0.0 (https://github.com/ansible-collections/community.crypto/issues/895,
https://github.com/ansible-collections/community.crypto/pull/901).
release_summary: Maintenance release announcing removal of the Entrust content
from community.crypto 3.0.0.
fragments:
- 2.26.2.yml
- 901-remove-entrust.yml
release_date: '2025-05-22'
2.26.3:
changes:
bugfixes:
- acme_account - make work with CAs that do not accept any account request
without External Account Binding data (https://github.com/ansible-collections/community.crypto/issues/918,
https://github.com/ansible-collections/community.crypto/pull/919).
release_summary: Bugfix release.
fragments:
- 2.26.3.yml
- 919-acme_account-ear.yml
release_date: '2025-06-14'
2.26.4:
changes:
bugfixes:
- Improve error message when loading a private key fails due to correct private
key files or wrong passwords. Also include the original cryptography error
since it likely contains more helpful information (https://github.com/ansible-collections/community.crypto/issues/936,
https://github.com/ansible-collections/community.crypto/pull/939).
release_summary: Bugfix release.
fragments:
- 2.26.4.yml
- 939-private-key-errors.yml
release_date: '2025-07-26'

View File

@@ -51,7 +51,7 @@ The following instructions show how to set up a simple self-signed CA certificat
Use the CA to sign a certificate 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>`. To sign a certificate, you must pass a CSR to the :ansplugin:`community.crypto.x509_certificate module <community.crypto.x509_certificate#module>` or :ansplugin:`community.crypto.x509_certificate_pipe module <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. 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.
@@ -94,7 +94,7 @@ In the following example, we assume that the certificate to sign (including its
delegate_to: server_1 delegate_to: server_1
run_once: true 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: 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 :ansplugin:`community.crypto.x509_certificate_pipe module <community.crypto.x509_certificate_pipe#module>`, and only writes the result back if it was changed:
.. code-block:: yaml+jinja .. code-block:: yaml+jinja

View File

@@ -10,7 +10,7 @@ How to create self-signed certificates
The `community.crypto collection <https://galaxy.ansible.com/ui/repo/published/community/crypto/>`_ offers multiple modules that create private keys, certificate signing requests, and certificates. This guide shows how to create self-signed certificates. The `community.crypto collection <https://galaxy.ansible.com/ui/repo/published/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 :ansopt:`community.crypto.openssl_privatekey#module:path`, the default parameters will be used. This will result in a 4096 bit RSA private key: For creating any kind of certificate, you always have to start with a private key. You can use the :ansplugin:`community.crypto.openssl_privatekey module <community.crypto.openssl_privatekey#module>` to create a private key. If you only specify :ansopt:`community.crypto.openssl_privatekey#module:path`, the default parameters will be used. This will result in a 4096 bit RSA private key:
.. code-block:: yaml+jinja .. code-block:: yaml+jinja
@@ -28,7 +28,7 @@ You can specify :ansopt:`community.crypto.openssl_privatekey#module:type` to sel
type: X25519 type: X25519
passphrase: changeme 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>`: To create a very simple self-signed certificate with no specific information, you can proceed directly with the :ansplugin:`community.crypto.x509_certificate module <community.crypto.x509_certificate#module>`:
.. code-block:: yaml+jinja .. code-block:: yaml+jinja
@@ -42,7 +42,7 @@ To create a very simple self-signed certificate with no specific information, yo
You can use :ansopt:`community.crypto.x509_certificate#module:selfsigned_not_after` to define when the certificate expires (default: in roughly 10 years), and :ansopt:`community.crypto.x509_certificate#module:selfsigned_not_before` to define from when the certificate is valid (default: now). You can use :ansopt:`community.crypto.x509_certificate#module:selfsigned_not_after` to define when the certificate expires (default: in roughly 10 years), and :ansopt:`community.crypto.x509_certificate#module: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.) 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 :ansplugin:`community.crypto.x509_certificate module <community.crypto.x509_certificate#module>`. If you do not need the CSR file, you can use the :ansplugin:`community.crypto.openssl_csr_pipe module <community.crypto.openssl_csr_pipe#module>` as in the example below. (To store it to disk, use the :ansplugin:`community.crypto.openssl_csr module <community.crypto.openssl_csr#module>` instead.)
.. code-block:: yaml+jinja .. code-block:: yaml+jinja

View File

@@ -5,7 +5,7 @@
namespace: community namespace: community
name: crypto name: crypto
version: 2.24.0 version: 2.26.4
readme: README.md readme: README.md
authors: authors:
- Ansible (github.com/ansible) - Ansible (github.com/ansible)
@@ -16,7 +16,7 @@ license:
- BSD-2-Clause - BSD-2-Clause
- BSD-3-Clause - BSD-3-Clause
- PSF-2.0 - PSF-2.0
#license_file: COPYING # license_file: COPYING
tags: tags:
- acme - acme
- certificate - certificate

40
noxfile.py Normal file
View File

@@ -0,0 +1,40 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2025 Felix Fontein <felix@fontein.de>
# /// script
# dependencies = ["nox>=2025.02.09", "antsibull-nox"]
# ///
import sys
import nox
try:
import antsibull_nox
except ImportError:
print("You need to install antsibull-nox in the same Python environment as nox.")
sys.exit(1)
antsibull_nox.load_antsibull_nox_toml()
@nox.session(name="create-certificates", default=False)
def create_certificates(session: nox.Session) -> None:
"""
Regenerate some vendored certificates.
"""
session.install("cryptography<39.0.0") # we want support for SHA1 signatures
session.run("python", "tests/create-certificates.py")
session.warn(
"Note that you need to modify some values in tests/integration/targets/x509_certificate_info/tasks/impl.yml"
" and tests/integration/targets/filter_x509_certificate_info/tasks/impl.yml!"
)
# Allow to run the noxfile with `python noxfile.py`, `pipx run noxfile.py`, or similar.
# Requires nox >= 2025.02.09
if __name__ == "__main__":
nox.main()

View File

@@ -5,22 +5,23 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import base64 import base64
from ansible.module_utils.common.text.converters import to_native, to_bytes from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible_collections.community.crypto.plugins.plugin_utils.action_module import ActionModuleBase
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.module_backends.privatekey import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey import (
select_backend,
get_privatekey_argument_spec, get_privatekey_argument_spec,
select_backend,
)
from ansible_collections.community.crypto.plugins.plugin_utils.action_module import (
ActionModuleBase,
) )
@@ -30,16 +31,18 @@ class PrivateKeyModule(object):
self.module_backend = module_backend self.module_backend = module_backend
self.check_mode = module.check_mode self.check_mode = module.check_mode
self.changed = False self.changed = False
self.return_current_key = module.params['return_current_key'] self.return_current_key = module.params["return_current_key"]
if module.params['content'] is not None: if module.params["content"] is not None:
if module.params['content_base64']: if module.params["content_base64"]:
try: try:
data = base64.b64decode(module.params['content']) data = base64.b64decode(module.params["content"])
except Exception as e: except Exception as e:
module.fail_json(msg='Cannot decode Base64 encoded data: {0}'.format(e)) module.fail_json(
msg="Cannot decode Base64 encoded data: {0}".format(e)
)
else: else:
data = to_bytes(module.params['content']) data = to_bytes(module.params["content"])
module_backend.set_existing(data) module_backend.set_existing(data)
def generate(self, module): def generate(self, module):
@@ -53,13 +56,13 @@ class PrivateKeyModule(object):
self.privatekey_bytes = privatekey_data self.privatekey_bytes = privatekey_data
else: else:
self.module.deprecate( self.module.deprecate(
'Check mode support for openssl_privatekey_pipe will change in community.crypto 3.0.0' "Check mode support for openssl_privatekey_pipe will change in community.crypto 3.0.0"
' to behave the same as without check mode. You can get that behavior right now' " to behave the same as without check mode. You can get that behavior right now"
' by adding `check_mode: false` to the openssl_privatekey_pipe task. If you think this' " by adding `check_mode: false` to the openssl_privatekey_pipe task. If you think this"
' breaks your use-case of this module, please create an issue in the' " breaks your use-case of this module, please create an issue in the"
' community.crypto repository', " community.crypto repository",
version='3.0.0', version="3.0.0",
collection_name='community.crypto', collection_name="community.crypto",
) )
self.changed = True self.changed = True
elif self.module_backend.needs_conversion(): elif self.module_backend.needs_conversion():
@@ -70,20 +73,22 @@ class PrivateKeyModule(object):
self.privatekey_bytes = privatekey_data self.privatekey_bytes = privatekey_data
else: else:
self.module.deprecate( self.module.deprecate(
'Check mode support for openssl_privatekey_pipe will change in community.crypto 3.0.0' "Check mode support for openssl_privatekey_pipe will change in community.crypto 3.0.0"
' to behave the same as without check mode. You can get that behavior right now' " to behave the same as without check mode. You can get that behavior right now"
' by adding `check_mode: false` to the openssl_privatekey_pipe task. If you think this' " by adding `check_mode: false` to the openssl_privatekey_pipe task. If you think this"
' breaks your use-case of this module, please create an issue in the' " breaks your use-case of this module, please create an issue in the"
' community.crypto repository', " community.crypto repository",
version='3.0.0', version="3.0.0",
collection_name='community.crypto', collection_name="community.crypto",
) )
self.changed = True self.changed = True
def dump(self): def dump(self):
"""Serialize the object into a dictionary.""" """Serialize the object into a dictionary."""
result = self.module_backend.dump(include_key=self.changed or self.return_current_key) result = self.module_backend.dump(
result['changed'] = self.changed include_key=self.changed or self.return_current_key
)
result["changed"] = self.changed
return result return result
@@ -91,11 +96,13 @@ class ActionModule(ActionModuleBase):
@staticmethod @staticmethod
def setup_module(): def setup_module():
argument_spec = get_privatekey_argument_spec() argument_spec = get_privatekey_argument_spec()
argument_spec.argument_spec.update(dict( argument_spec.argument_spec.update(
content=dict(type='str', no_log=True), dict(
content_base64=dict(type='bool', default=False), content=dict(type="str", no_log=True),
return_current_key=dict(type='bool', default=False), content_base64=dict(type="bool", default=False),
)) return_current_key=dict(type="bool", default=False),
)
)
return argument_spec, dict( return argument_spec, dict(
supports_check_mode=True, supports_check_mode=True,
) )
@@ -104,7 +111,7 @@ class ActionModule(ActionModuleBase):
def run_module(module): def run_module(module):
backend, module_backend = select_backend( backend, module_backend = select_backend(
module=module, module=module,
backend=module.params['select_crypto_backend'], backend=module.params["select_crypto_backend"],
) )
try: try:
@@ -119,10 +126,10 @@ class ActionModule(ActionModuleBase):
# `module.no_log = True`, this should be safe. # `module.no_log = True`, this should be safe.
module.no_log = True module.no_log = True
try: try:
module.no_log_values.remove(module.params['content']) module.no_log_values.remove(module.params["content"])
except KeyError: except KeyError:
pass pass
module.params['content'] = 'ANSIBLE_NO_LOG_VALUE' module.params["content"] = "ANSIBLE_NO_LOG_VALUE"
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))

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -118,7 +120,7 @@ notes:
the module can in principle be used with any CA providing an ACME endpoint, such as L(Buypass Go SSL,https://www.buypass.com/ssl/products/acme). the module can in principle be used with any CA providing an ACME endpoint, such as L(Buypass Go SSL,https://www.buypass.com/ssl/products/acme).
- So far, the ACME modules have only been tested by the developers against Let's Encrypt (staging and production), Buypass - So far, the ACME modules have only been tested by the developers against Let's Encrypt (staging and production), Buypass
(staging and production), ZeroSSL (production), and L(Pebble testing server,https://github.com/letsencrypt/Pebble). We (staging and production), ZeroSSL (production), and L(Pebble testing server,https://github.com/letsencrypt/Pebble). We
have got community feedback that they also work with Sectigo ACME Service for InCommon. If you experience problems with have got community feedback that they also work with Sectigo ACME Service for InCommon and with HARICA. If you experience problems with
another ACME server, please L(create an issue,https://github.com/ansible-collections/community.crypto/issues/new/choose) another ACME server, please L(create an issue,https://github.com/ansible-collections/community.crypto/issues/new/choose)
to help us supporting it. Feedback that an ACME server not mentioned does work is also appreciated. to help us supporting it. Feedback that an ACME server not mentioned does work is also appreciated.
requirements: requirements:
@@ -144,6 +146,7 @@ options:
- For B(Buypass), the production directory URL for ACME v2 and v1 is U(https://api.buypass.com/acme/directory). - For B(Buypass), the production directory URL for ACME v2 and v1 is U(https://api.buypass.com/acme/directory).
- For B(ZeroSSL), the production directory URL for ACME v2 is U(https://acme.zerossl.com/v2/DV90). - For B(ZeroSSL), the production directory URL for ACME v2 is U(https://acme.zerossl.com/v2/DV90).
- For B(Sectigo), the production directory URL for ACME v2 is U(https://acme-qa.secure.trust-provider.com/v2/DV). - For B(Sectigo), the production directory URL for ACME v2 is U(https://acme-qa.secure.trust-provider.com/v2/DV).
- For B(HARICA), the production directory URL for ACME v2 is U(https://acme.harica.gr/XXX/directory) with XXX being specific to your account.
- The notes for this module contain a list of ACME services this module has been tested against. - The notes for this module contain a list of ACME services this module has been tested against.
required: true required: true
type: str type: str
@@ -183,6 +186,7 @@ options:
account_key_src: account_key_src:
description: description:
- Path to a file containing the ACME account RSA or Elliptic Curve key. - Path to a file containing the ACME account RSA or Elliptic Curve key.
- "For Elliptic Curve keys only the following curves are supported: V(secp256r1), V(secp384r1), and V(secp521r1)."
- 'Private keys can be created with the M(community.crypto.openssl_privatekey) or M(community.crypto.openssl_privatekey_pipe) - 'Private keys can be created with the M(community.crypto.openssl_privatekey) or M(community.crypto.openssl_privatekey_pipe)
modules. If the requisite (cryptography) is not available, keys can also be created directly with the C(openssl) command modules. If the requisite (cryptography) is not available, keys can also be created directly with the C(openssl) command
line tool: RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys can be created with C(openssl ecparam line tool: RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys can be created with C(openssl ecparam
@@ -190,10 +194,12 @@ options:
- Mutually exclusive with O(account_key_content). - Mutually exclusive with O(account_key_content).
- Required if O(account_key_content) is not used. - Required if O(account_key_content) is not used.
type: path type: path
aliases: [account_key] aliases:
- account_key
account_key_content: account_key_content:
description: description:
- Content of the ACME account RSA or Elliptic Curve key. - Content of the ACME account RSA or Elliptic Curve key.
- "For Elliptic Curve keys only the following curves are supported: V(secp256r1), V(secp384r1), and V(secp521r1)."
- Mutually exclusive with O(account_key_src). - Mutually exclusive with O(account_key_src).
- Required if O(account_key_src) is not used. - Required if O(account_key_src) is not used.
- B(Warning:) the content will be written into a temporary file, which will be deleted by Ansible when the module completes. - B(Warning:) the content will be written into a temporary file, which will be deleted by Ansible when the module completes.
@@ -216,7 +222,7 @@ options:
""" """
# No account data documentation fragment # No account data documentation fragment
NO_ACCOUNT = r''' NO_ACCOUNT = r"""
notes: notes:
- "If a new enough version of the C(cryptography) library - "If a new enough version of the C(cryptography) library
is available (see Requirements for details), it will be used is available (see Requirements for details), it will be used
@@ -224,7 +230,7 @@ notes:
or enabled with the O(select_crypto_backend) option. Note that using or enabled with the O(select_crypto_backend) option. Note that using
the C(openssl) binary will be slower." the C(openssl) binary will be slower."
options: {} options: {}
''' """
CERTIFICATE = r""" CERTIFICATE = r"""
options: options:

View File

@@ -4,7 +4,9 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -35,7 +37,7 @@ attributes:
""" """
# Should be used together with the standard fragment # Should be used together with the standard fragment
INFO_MODULE = r''' INFO_MODULE = r"""
options: {} options: {}
attributes: attributes:
check_mode: check_mode:
@@ -46,9 +48,9 @@ attributes:
support: N/A support: N/A
details: details:
- This action does not modify state. - This action does not modify state.
''' """
ACTIONGROUP_ACME = r''' ACTIONGROUP_ACME = r"""
options: {} options: {}
attributes: attributes:
action_group: action_group:
@@ -57,7 +59,7 @@ attributes:
membership: membership:
- community.crypto.acme - community.crypto.acme
- acme - acme
''' """
FACTS = r""" FACTS = r"""
options: {} options: {}
@@ -67,7 +69,7 @@ attributes:
""" """
# Should be used together with the standard fragment and the FACTS fragment # Should be used together with the standard fragment and the FACTS fragment
FACTS_MODULE = r''' FACTS_MODULE = r"""
options: {} options: {}
attributes: attributes:
check_mode: check_mode:
@@ -80,7 +82,7 @@ attributes:
- This action does not modify state. - This action does not modify state.
facts: facts:
support: full support: full
''' """
FILES = r""" FILES = r"""
options: {} options: {}

View File

@@ -4,7 +4,9 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -94,7 +96,7 @@ seealso:
- module: community.crypto.openssl_publickey - module: community.crypto.openssl_publickey
""" """
BACKEND_ACME_DOCUMENTATION = r''' BACKEND_ACME_DOCUMENTATION = r"""
description: description:
- This module allows one to (re)generate OpenSSL certificates. - This module allows one to (re)generate OpenSSL certificates.
requirements: requirements:
@@ -127,9 +129,9 @@ options:
- "Let's Encrypt recommends using their staging server while developing jobs. U(https://letsencrypt.org/docs/staging-environment/)." - "Let's Encrypt recommends using their staging server while developing jobs. U(https://letsencrypt.org/docs/staging-environment/)."
type: str type: str
default: https://acme-v02.api.letsencrypt.org/directory default: https://acme-v02.api.letsencrypt.org/directory
''' """
BACKEND_ENTRUST_DOCUMENTATION = r''' BACKEND_ENTRUST_DOCUMENTATION = r"""
options: options:
entrust_cert_type: entrust_cert_type:
description: description:
@@ -212,9 +214,9 @@ options:
- This is only used by the V(entrust) provider. - This is only used by the V(entrust) provider.
type: path type: path
default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
''' """
BACKEND_OWNCA_DOCUMENTATION = r''' BACKEND_OWNCA_DOCUMENTATION = r"""
description: description:
- The V(ownca) provider is intended for generating an OpenSSL certificate signed with your own - The V(ownca) provider is intended for generating an OpenSSL certificate signed with your own
CA (Certificate Authority) certificate (self-signed certificate). CA (Certificate Authority) certificate (self-signed certificate).
@@ -322,9 +324,9 @@ options:
- 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!
type: bool type: bool
default: true default: true
''' """
BACKEND_SELFSIGNED_DOCUMENTATION = r''' BACKEND_SELFSIGNED_DOCUMENTATION = r"""
notes: notes:
- For the V(selfsigned) provider, O(csr_path) and O(csr_content) are optional. If not provided, a - For the V(selfsigned) provider, O(csr_path) and O(csr_content) are optional. If not provided, a
certificate without any information (Subject, Subject Alternative Names, Key Usage, etc.) is created. certificate without any information (Subject, Subject Alternative Names, Key Usage, etc.) is created.
@@ -375,7 +377,8 @@ options:
- This is only used by the V(selfsigned) provider. - This is only used by the V(selfsigned) provider.
type: str type: str
default: +0s default: +0s
aliases: [ selfsigned_notBefore ] aliases:
- selfsigned_notBefore
selfsigned_not_after: selfsigned_not_after:
description: description:
@@ -393,7 +396,8 @@ options:
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.
type: str type: str
default: +3650d default: +3650d
aliases: [ selfsigned_notAfter ] aliases:
- selfsigned_notAfter
selfsigned_create_subject_key_identifier: selfsigned_create_subject_key_identifier:
description: description:
@@ -408,4 +412,4 @@ options:
type: str type: str
choices: [create_if_not_provided, always_create, never_create] choices: [create_if_not_provided, always_create, never_create]
default: create_if_not_provided default: create_if_not_provided
''' """

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -73,37 +75,51 @@ options:
description: description:
- The countryName field of the certificate signing request subject. - The countryName field of the certificate signing request subject.
type: str type: str
aliases: [C, countryName] aliases:
- C
- countryName
state_or_province_name: state_or_province_name:
description: description:
- The stateOrProvinceName field of the certificate signing request subject. - The stateOrProvinceName field of the certificate signing request subject.
type: str type: str
aliases: [ST, stateOrProvinceName] aliases:
- ST
- stateOrProvinceName
locality_name: locality_name:
description: description:
- The localityName field of the certificate signing request subject. - The localityName field of the certificate signing request subject.
type: str type: str
aliases: [L, localityName] aliases:
- L
- localityName
organization_name: organization_name:
description: description:
- The organizationName field of the certificate signing request subject. - The organizationName field of the certificate signing request subject.
type: str type: str
aliases: [O, organizationName] aliases:
- O
- organizationName
organizational_unit_name: organizational_unit_name:
description: description:
- The organizationalUnitName field of the certificate signing request subject. - The organizationalUnitName field of the certificate signing request subject.
type: str type: str
aliases: [OU, organizationalUnitName] aliases:
- OU
- organizationalUnitName
common_name: common_name:
description: description:
- The commonName field of the certificate signing request subject. - The commonName field of the certificate signing request subject.
type: str type: str
aliases: [CN, commonName] aliases:
- CN
- commonName
email_address: email_address:
description: description:
- The emailAddress field of the certificate signing request subject. - The emailAddress field of the certificate signing request subject.
type: str type: str
aliases: [E, emailAddress] aliases:
- E
- emailAddress
subject_alt_name: subject_alt_name:
description: description:
- Subject Alternative Name (SAN) extension to attach to the certificate signing request. - Subject Alternative Name (SAN) extension to attach to the certificate signing request.
@@ -114,63 +130,75 @@ options:
- More at U(https://tools.ietf.org/html/rfc5280#section-4.2.1.6). - More at U(https://tools.ietf.org/html/rfc5280#section-4.2.1.6).
type: list type: list
elements: str elements: str
aliases: [subjectAltName] aliases:
- subjectAltName
subject_alt_name_critical: subject_alt_name_critical:
description: description:
- Should the subjectAltName extension be considered as critical. - Should the subjectAltName extension be considered as critical.
type: bool type: bool
default: false default: false
aliases: [subjectAltName_critical] aliases:
- subjectAltName_critical
use_common_name_for_san: use_common_name_for_san:
description: description:
- If set to V(true), the module will fill the common name in for O(subject_alt_name) with C(DNS:) prefix if no SAN is - If set to V(true), the module will fill the common name in for O(subject_alt_name) with C(DNS:) prefix if no SAN is
specified. specified.
type: bool type: bool
default: true default: true
aliases: [useCommonNameForSAN] aliases:
- useCommonNameForSAN
key_usage: key_usage:
description: description:
- This defines the purpose (for example encipherment, signature, certificate signing) of the key contained in the certificate. - This defines the purpose (for example encipherment, signature, certificate signing) of the key contained in the certificate.
type: list type: list
elements: str elements: str
aliases: [keyUsage] aliases:
- keyUsage
key_usage_critical: key_usage_critical:
description: description:
- Should the keyUsage extension be considered as critical. - Should the keyUsage extension be considered as critical.
type: bool type: bool
default: false default: false
aliases: [keyUsage_critical] aliases:
- keyUsage_critical
extended_key_usage: extended_key_usage:
description: description:
- Additional restrictions (for example client authentication, server authentication) on the allowed purposes for which - Additional restrictions (for example client authentication, server authentication) on the allowed purposes for which
the public key may be used. the public key may be used.
type: list type: list
elements: str elements: str
aliases: [extKeyUsage, extendedKeyUsage] aliases:
- extKeyUsage
- extendedKeyUsage
extended_key_usage_critical: extended_key_usage_critical:
description: description:
- Should the extkeyUsage extension be considered as critical. - Should the extkeyUsage extension be considered as critical.
type: bool type: bool
default: false default: false
aliases: [extKeyUsage_critical, extendedKeyUsage_critical] aliases:
- extKeyUsage_critical
- extendedKeyUsage_critical
basic_constraints: basic_constraints:
description: description:
- Indicates basic constraints, such as if the certificate is a CA. - Indicates basic constraints, such as if the certificate is a CA.
type: list type: list
elements: str elements: str
aliases: [basicConstraints] aliases:
- basicConstraints
basic_constraints_critical: basic_constraints_critical:
description: description:
- Should the basicConstraints extension be considered as critical. - Should the basicConstraints extension be considered as critical.
type: bool type: bool
default: false default: false
aliases: [basicConstraints_critical] aliases:
- basicConstraints_critical
ocsp_must_staple: ocsp_must_staple:
description: description:
- Indicates that the certificate should contain the OCSP Must Staple extension (U(https://tools.ietf.org/html/rfc7633)). - Indicates that the certificate should contain the OCSP Must Staple extension (U(https://tools.ietf.org/html/rfc7633)).
type: bool type: bool
default: false default: false
aliases: [ocspMustStaple] aliases:
- ocspMustStaple
ocsp_must_staple_critical: ocsp_must_staple_critical:
description: description:
- Should the OCSP Must Staple extension be considered as critical. - Should the OCSP Must Staple extension be considered as critical.
@@ -178,7 +206,8 @@ options:
OCSP Must Staple are required to reject such certificates (see U(https://tools.ietf.org/html/rfc7633#section-4)). OCSP Must Staple are required to reject such certificates (see U(https://tools.ietf.org/html/rfc7633#section-4)).
type: bool type: bool
default: false default: false
aliases: [ocspMustStaple_critical] aliases:
- ocspMustStaple_critical
name_constraints_permitted: name_constraints_permitted:
description: description:
- For CA certificates, this specifies a list of identifiers which describe subtrees of names that this CA is allowed - For CA certificates, this specifies a list of identifiers which describe subtrees of names that this CA is allowed

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type

View File

@@ -3,7 +3,9 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r""" DOCUMENTATION = r"""
@@ -27,6 +29,7 @@ seealso:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Show fingerprint of GPG public key - name: Show fingerprint of GPG public key
ansible.builtin.debug: ansible.builtin.debug:
msg: "{{ lookup('file', '/path/to/public_key.gpg') | community.crypto.gpg_fingerprint }}" msg: "{{ lookup('file', '/path/to/public_key.gpg') | community.crypto.gpg_fingerprint }}"
@@ -42,15 +45,21 @@ _value:
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.gnupg.cli import (
from ansible_collections.community.crypto.plugins.module_utils.gnupg.cli import GPGError, get_fingerprint_from_bytes GPGError,
from ansible_collections.community.crypto.plugins.plugin_utils.gnupg import PluginGPGRunner get_fingerprint_from_bytes,
)
from ansible_collections.community.crypto.plugins.plugin_utils.gnupg import (
PluginGPGRunner,
)
def gpg_fingerprint(input): def gpg_fingerprint(input):
if not isinstance(input, string_types): if not isinstance(input, string_types):
raise AnsibleFilterError( raise AnsibleFilterError(
'The input for the community.crypto.gpg_fingerprint filter must be a string; got {type} instead'.format(type=type(input)) "The input for the community.crypto.gpg_fingerprint filter must be a string; got {type} instead".format(
type=type(input)
)
) )
try: try:
gpg = PluginGPGRunner() gpg = PluginGPGRunner()
@@ -60,9 +69,9 @@ def gpg_fingerprint(input):
class FilterModule(object): class FilterModule(object):
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self):
return { return {
'gpg_fingerprint': gpg_fingerprint, "gpg_fingerprint": gpg_fingerprint,
} }

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r""" DOCUMENTATION = r"""
@@ -31,6 +33,7 @@ seealso:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Show the Subject Alt Names of the CSR - name: Show the Subject Alt Names of the CSR
ansible.builtin.debug: ansible.builtin.debug:
msg: >- msg: >-
@@ -276,41 +279,51 @@ _value:
""" """
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
from ansible.module_utils.six import string_types
from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
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.module_backends.csr_info import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import (
get_csr_info, get_csr_info,
) )
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import (
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock FilterModuleMock,
)
def openssl_csr_info_filter(data, name_encoding='ignore'): def openssl_csr_info_filter(data, name_encoding="ignore"):
'''Extract information from X.509 PEM certificate.''' """Extract information from X.509 PEM certificate."""
if not isinstance(data, string_types): if not isinstance(data, string_types):
raise AnsibleFilterError('The community.crypto.openssl_csr_info input must be a text type, not %s' % type(data)) raise AnsibleFilterError(
"The community.crypto.openssl_csr_info input must be a text type, not %s"
% type(data)
)
if not isinstance(name_encoding, string_types): if not isinstance(name_encoding, string_types):
raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding)) raise AnsibleFilterError(
"The name_encoding option must be of a text type, not %s"
% type(name_encoding)
)
name_encoding = to_native(name_encoding) name_encoding = to_native(name_encoding)
if name_encoding not in ('ignore', 'idna', 'unicode'): if name_encoding not in ("ignore", "idna", "unicode"):
raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding) raise AnsibleFilterError(
'The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"'
% name_encoding
)
module = FilterModuleMock({'name_encoding': name_encoding}) module = FilterModuleMock({"name_encoding": name_encoding})
try: try:
return get_csr_info(module, 'cryptography', content=to_bytes(data), validate_signature=True) return get_csr_info(
module, "cryptography", content=to_bytes(data), validate_signature=True
)
except OpenSSLObjectError as exc: except OpenSSLObjectError as exc:
raise AnsibleFilterError(to_native(exc)) raise AnsibleFilterError(to_native(exc))
class FilterModule(object): class FilterModule(object):
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self):
return { return {
'openssl_csr_info': openssl_csr_info_filter, "openssl_csr_info": openssl_csr_info_filter,
} }

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r""" DOCUMENTATION = r"""
@@ -40,6 +42,7 @@ seealso:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Show the Subject Alt Names of the CSR - name: Show the Subject Alt Names of the CSR
ansible.builtin.debug: ansible.builtin.debug:
msg: >- msg: >-
@@ -148,35 +151,50 @@ _value:
""" """
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
from ansible.module_utils.six import string_types
from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
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.module_backends.privatekey_info import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import (
PrivateKeyParseError, PrivateKeyParseError,
get_privatekey_info, get_privatekey_info,
) )
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import (
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock FilterModuleMock,
)
def openssl_privatekey_info_filter(data, passphrase=None, return_private_key_data=False): def openssl_privatekey_info_filter(
'''Extract information from X.509 PEM certificate.''' data, passphrase=None, return_private_key_data=False
):
"""Extract information from X.509 PEM certificate."""
if not isinstance(data, string_types): if not isinstance(data, string_types):
raise AnsibleFilterError('The community.crypto.openssl_privatekey_info input must be a text type, not %s' % type(data)) raise AnsibleFilterError(
"The community.crypto.openssl_privatekey_info input must be a text type, not %s"
% type(data)
)
if passphrase is not None and not isinstance(passphrase, string_types): if passphrase is not None and not isinstance(passphrase, string_types):
raise AnsibleFilterError('The passphrase option must be a text type, not %s' % type(passphrase)) raise AnsibleFilterError(
"The passphrase option must be a text type, not %s" % type(passphrase)
)
if not isinstance(return_private_key_data, bool): if not isinstance(return_private_key_data, bool):
raise AnsibleFilterError('The return_private_key_data option must be a boolean, not %s' % type(return_private_key_data)) raise AnsibleFilterError(
"The return_private_key_data option must be a boolean, not %s"
% type(return_private_key_data)
)
module = FilterModuleMock({}) module = FilterModuleMock({})
try: try:
result = get_privatekey_info(module, 'cryptography', content=to_bytes(data), passphrase=passphrase, return_private_key_data=return_private_key_data) result = get_privatekey_info(
result.pop('can_parse_key', None) module,
result.pop('key_is_consistent', None) "cryptography",
content=to_bytes(data),
passphrase=passphrase,
return_private_key_data=return_private_key_data,
)
result.pop("can_parse_key", None)
result.pop("key_is_consistent", None)
return result return result
except PrivateKeyParseError as exc: except PrivateKeyParseError as exc:
raise AnsibleFilterError(exc.error_message) raise AnsibleFilterError(exc.error_message)
@@ -185,9 +203,9 @@ def openssl_privatekey_info_filter(data, passphrase=None, return_private_key_dat
class FilterModule(object): class FilterModule(object):
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self):
return { return {
'openssl_privatekey_info': openssl_privatekey_info_filter, "openssl_privatekey_info": openssl_privatekey_info_filter,
} }

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r""" DOCUMENTATION = r"""
@@ -27,6 +29,7 @@ seealso:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Show the type of a public key - name: Show the type of a public key
ansible.builtin.debug: ansible.builtin.debug:
msg: >- msg: >-
@@ -125,29 +128,31 @@ _value:
""" """
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
from ansible.module_utils.six import string_types
from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
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.module_backends.publickey_info import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
PublicKeyParseError, PublicKeyParseError,
get_publickey_info, get_publickey_info,
) )
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import (
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock FilterModuleMock,
)
def openssl_publickey_info_filter(data): def openssl_publickey_info_filter(data):
'''Extract information from OpenSSL PEM public key.''' """Extract information from OpenSSL PEM public key."""
if not isinstance(data, string_types): if not isinstance(data, string_types):
raise AnsibleFilterError('The community.crypto.openssl_publickey_info input must be a text type, not %s' % type(data)) raise AnsibleFilterError(
"The community.crypto.openssl_publickey_info input must be a text type, not %s"
% type(data)
)
module = FilterModuleMock({}) module = FilterModuleMock({})
try: try:
return get_publickey_info(module, 'cryptography', content=to_bytes(data)) return get_publickey_info(module, "cryptography", content=to_bytes(data))
except PublicKeyParseError as exc: except PublicKeyParseError as exc:
raise AnsibleFilterError(exc.error_message) raise AnsibleFilterError(exc.error_message)
except OpenSSLObjectError as exc: except OpenSSLObjectError as exc:
@@ -155,9 +160,9 @@ def openssl_publickey_info_filter(data):
class FilterModule(object): class FilterModule(object):
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self):
return { return {
'openssl_publickey_info': openssl_publickey_info_filter, "openssl_publickey_info": openssl_publickey_info_filter,
} }

View File

@@ -3,7 +3,9 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r""" DOCUMENTATION = r"""
@@ -27,6 +29,7 @@ seealso:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Parse serial number - name: Parse serial number
ansible.builtin.debug: ansible.builtin.debug:
msg: "{{ '11:22:33' | community.crypto.parse_serial }}" msg: "{{ '11:22:33' | community.crypto.parse_serial }}"
@@ -42,14 +45,17 @@ _value:
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.serial import (
from ansible_collections.community.crypto.plugins.module_utils.serial import parse_serial parse_serial,
)
def parse_serial_filter(input): def parse_serial_filter(input):
if not isinstance(input, string_types): if not isinstance(input, string_types):
raise AnsibleFilterError( raise AnsibleFilterError(
'The input for the community.crypto.parse_serial filter must be a string; got {type} instead'.format(type=type(input)) "The input for the community.crypto.parse_serial filter must be a string; got {type} instead".format(
type=type(input)
)
) )
try: try:
return parse_serial(to_native(input)) return parse_serial(to_native(input))
@@ -58,9 +64,9 @@ def parse_serial_filter(input):
class FilterModule(object): class FilterModule(object):
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self):
return { return {
'parse_serial': parse_serial_filter, "parse_serial": parse_serial_filter,
} }

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r""" DOCUMENTATION = r"""
@@ -24,6 +26,7 @@ options:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Print all CA certificates - name: Print all CA certificates
ansible.builtin.debug: ansible.builtin.debug:
msg: '{{ item }}' msg: '{{ item }}'
@@ -40,25 +43,29 @@ _value:
""" """
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
from ansible.module_utils.six import string_types
from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.common.text.converters import to_text
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import split_pem_list from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
split_pem_list,
)
def split_pem_filter(data): def split_pem_filter(data):
'''Split PEM file.''' """Split PEM file."""
if not isinstance(data, string_types): if not isinstance(data, string_types):
raise AnsibleFilterError('The community.crypto.split_pem input must be a text type, not %s' % type(data)) raise AnsibleFilterError(
"The community.crypto.split_pem input must be a text type, not %s"
% type(data)
)
data = to_text(data) data = to_text(data)
return split_pem_list(data) return split_pem_list(data)
class FilterModule(object): class FilterModule(object):
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self):
return { return {
'split_pem': split_pem_filter, "split_pem": split_pem_filter,
} }

View File

@@ -3,7 +3,9 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r""" DOCUMENTATION = r"""
@@ -25,6 +27,7 @@ seealso:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Convert integer to serial number - name: Convert integer to serial number
ansible.builtin.debug: ansible.builtin.debug:
msg: "{{ 1234567 | community.crypto.to_serial }}" msg: "{{ 1234567 | community.crypto.to_serial }}"
@@ -42,17 +45,20 @@ _value:
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import integer_types from ansible.module_utils.six import integer_types
from ansible_collections.community.crypto.plugins.module_utils.serial import to_serial from ansible_collections.community.crypto.plugins.module_utils.serial import to_serial
def to_serial_filter(input): def to_serial_filter(input):
if not isinstance(input, integer_types): if not isinstance(input, integer_types):
raise AnsibleFilterError( raise AnsibleFilterError(
'The input for the community.crypto.to_serial filter must be an integer; got {type} instead'.format(type=type(input)) "The input for the community.crypto.to_serial filter must be an integer; got {type} instead".format(
type=type(input)
)
) )
if input < 0: if input < 0:
raise AnsibleFilterError('The input for the community.crypto.to_serial filter must not be negative') raise AnsibleFilterError(
"The input for the community.crypto.to_serial filter must not be negative"
)
try: try:
return to_serial(input) return to_serial(input)
except ValueError as exc: except ValueError as exc:
@@ -60,9 +66,9 @@ def to_serial_filter(input):
class FilterModule(object): class FilterModule(object):
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self):
return { return {
'to_serial': to_serial_filter, "to_serial": to_serial_filter,
} }

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r""" DOCUMENTATION = r"""
@@ -31,6 +33,7 @@ seealso:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Show the Subject Alt Names of the certificate - name: Show the Subject Alt Names of the certificate
ansible.builtin.debug: ansible.builtin.debug:
msg: >- msg: >-
@@ -310,41 +313,49 @@ _value:
""" """
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
from ansible.module_utils.six import string_types
from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
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.module_backends.certificate_info import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
get_certificate_info, get_certificate_info,
) )
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import (
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock FilterModuleMock,
)
def x509_certificate_info_filter(data, name_encoding='ignore'): def x509_certificate_info_filter(data, name_encoding="ignore"):
'''Extract information from X.509 PEM certificate.''' """Extract information from X.509 PEM certificate."""
if not isinstance(data, string_types): if not isinstance(data, string_types):
raise AnsibleFilterError('The community.crypto.x509_certificate_info input must be a text type, not %s' % type(data)) raise AnsibleFilterError(
"The community.crypto.x509_certificate_info input must be a text type, not %s"
% type(data)
)
if not isinstance(name_encoding, string_types): if not isinstance(name_encoding, string_types):
raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding)) raise AnsibleFilterError(
"The name_encoding option must be of a text type, not %s"
% type(name_encoding)
)
name_encoding = to_native(name_encoding) name_encoding = to_native(name_encoding)
if name_encoding not in ('ignore', 'idna', 'unicode'): if name_encoding not in ("ignore", "idna", "unicode"):
raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding) raise AnsibleFilterError(
'The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"'
% name_encoding
)
module = FilterModuleMock({'name_encoding': name_encoding}) module = FilterModuleMock({"name_encoding": name_encoding})
try: try:
return get_certificate_info(module, 'cryptography', content=to_bytes(data)) return get_certificate_info(module, "cryptography", content=to_bytes(data))
except OpenSSLObjectError as exc: except OpenSSLObjectError as exc:
raise AnsibleFilterError(to_native(exc)) raise AnsibleFilterError(to_native(exc))
class FilterModule(object): class FilterModule(object):
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self):
return { return {
'x509_certificate_info': x509_certificate_info_filter, "x509_certificate_info": x509_certificate_info_filter,
} }

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r""" DOCUMENTATION = r"""
@@ -39,6 +41,7 @@ seealso:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Show the Organization Name of the CRL's subject - name: Show the Organization Name of the CRL's subject
ansible.builtin.debug: ansible.builtin.debug:
msg: >- msg: >-
@@ -158,54 +161,66 @@ import base64
import binascii import binascii
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
from ansible.module_utils.six import string_types
from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
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.pem import (
identify_pem_format,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import (
get_crl_info, get_crl_info,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock identify_pem_format,
)
from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import (
FilterModuleMock,
)
def x509_crl_info_filter(data, name_encoding='ignore', list_revoked_certificates=True): def x509_crl_info_filter(data, name_encoding="ignore", list_revoked_certificates=True):
'''Extract information from X.509 PEM certificate.''' """Extract information from X.509 PEM certificate."""
if not isinstance(data, string_types): if not isinstance(data, string_types):
raise AnsibleFilterError('The community.crypto.x509_crl_info input must be a text type, not %s' % type(data)) raise AnsibleFilterError(
"The community.crypto.x509_crl_info input must be a text type, not %s"
% type(data)
)
if not isinstance(name_encoding, string_types): if not isinstance(name_encoding, string_types):
raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding)) raise AnsibleFilterError(
"The name_encoding option must be of a text type, not %s"
% type(name_encoding)
)
if not isinstance(list_revoked_certificates, bool): if not isinstance(list_revoked_certificates, bool):
raise AnsibleFilterError('The list_revoked_certificates option must be a boolean, not %s' % type(list_revoked_certificates)) raise AnsibleFilterError(
"The list_revoked_certificates option must be a boolean, not %s"
% type(list_revoked_certificates)
)
name_encoding = to_native(name_encoding) name_encoding = to_native(name_encoding)
if name_encoding not in ('ignore', 'idna', 'unicode'): if name_encoding not in ("ignore", "idna", "unicode"):
raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding) raise AnsibleFilterError(
'The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"'
% name_encoding
)
data = to_bytes(data) data = to_bytes(data)
if not identify_pem_format(data): if not identify_pem_format(data):
try: try:
data = base64.b64decode(to_native(data)) data = base64.b64decode(to_native(data))
except (binascii.Error, TypeError, ValueError, UnicodeEncodeError) as e: except (binascii.Error, TypeError, ValueError, UnicodeEncodeError):
pass pass
module = FilterModuleMock({'name_encoding': name_encoding}) module = FilterModuleMock({"name_encoding": name_encoding})
try: try:
return get_crl_info(module, content=data, list_revoked_certificates=list_revoked_certificates) return get_crl_info(
module, content=data, list_revoked_certificates=list_revoked_certificates
)
except OpenSSLObjectError as exc: except OpenSSLObjectError as exc:
raise AnsibleFilterError(to_native(exc)) raise AnsibleFilterError(to_native(exc))
class FilterModule(object): class FilterModule(object):
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self):
return { return {
'x509_crl_info': x509_crl_info_filter, "x509_crl_info": x509_crl_info_filter,
} }

View File

@@ -3,7 +3,9 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r""" DOCUMENTATION = r"""
@@ -28,6 +30,7 @@ seealso:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Show fingerprint of GPG public key - name: Show fingerprint of GPG public key
ansible.builtin.debug: ansible.builtin.debug:
msg: "{{ lookup('community.crypto.gpg_fingerprint', '/path/to/public_key.gpg') }}" msg: "{{ lookup('community.crypto.gpg_fingerprint', '/path/to/public_key.gpg') }}"
@@ -42,12 +45,16 @@ _value:
elements: string elements: string
""" """
from ansible.plugins.lookup import LookupBase
from ansible.errors import AnsibleLookupError from ansible.errors import AnsibleLookupError
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible.plugins.lookup import LookupBase
from ansible_collections.community.crypto.plugins.module_utils.gnupg.cli import GPGError, get_fingerprint_from_file from ansible_collections.community.crypto.plugins.module_utils.gnupg.cli import (
from ansible_collections.community.crypto.plugins.plugin_utils.gnupg import PluginGPGRunner GPGError,
get_fingerprint_from_file,
)
from ansible_collections.community.crypto.plugins.plugin_utils.gnupg import (
PluginGPGRunner,
)
class LookupModule(LookupBase): class LookupModule(LookupBase):

View File

@@ -27,11 +27,14 @@ Every version number class implements the following interface:
of the same class, thus must follow the same rules) of the same class, thus must follow the same rules)
""" """
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import re import re
try: try:
RE_FLAGS = re.VERBOSE | re.ASCII RE_FLAGS = re.VERBOSE | re.ASCII
except AttributeError: except AttributeError:

View File

@@ -6,11 +6,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
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._collections_compat import Mapping from ansible.module_utils.common._collections_compat import Mapping
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ACMEProtocolException, ACMEProtocolException,
ModuleFailException, ModuleFailException,
@@ -18,10 +19,10 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
class ACMEAccount(object): class ACMEAccount(object):
''' """
ACME account object. Allows to create new accounts, check for existence of accounts, ACME account object. Allows to create new accounts, check for existence of accounts,
retrieve account data. retrieve account data.
''' """
def __init__(self, client): def __init__(self, client):
# Set to true to enable logging of all signed requests # Set to true to enable logging of all signed requests
@@ -29,9 +30,15 @@ class ACMEAccount(object):
self.client = client self.client = client
def _new_reg(self, contact=None, agreement=None, terms_agreed=False, allow_creation=True, def _new_reg(
external_account_binding=None): self,
''' contact=None,
agreement=None,
terms_agreed=False,
allow_creation=True,
external_account_binding=None,
):
"""
Registers a new ACME account. Returns a pair ``(created, data)``. Registers a new ACME account. Returns a pair ``(created, data)``.
Here, ``created`` is ``True`` if the account was created and Here, ``created`` is ``True`` if the account was created and
``False`` if it already existed (e.g. it was not newly created), ``False`` if it already existed (e.g. it was not newly created),
@@ -43,27 +50,33 @@ class ACMEAccount(object):
(https://tools.ietf.org/html/rfc8555#section-7.3.4). (https://tools.ietf.org/html/rfc8555#section-7.3.4).
https://tools.ietf.org/html/rfc8555#section-7.3 https://tools.ietf.org/html/rfc8555#section-7.3
''' """
contact = contact or [] contact = contact or []
if self.client.version == 1: if self.client.version == 1:
new_reg = { new_reg = {"resource": "new-reg", "contact": contact}
'resource': 'new-reg',
'contact': contact
}
if agreement: if agreement:
new_reg['agreement'] = agreement new_reg["agreement"] = agreement
else: else:
new_reg['agreement'] = self.client.directory['meta']['terms-of-service'] new_reg["agreement"] = self.client.directory["meta"]["terms-of-service"]
if external_account_binding is not None: if external_account_binding is not None:
raise ModuleFailException('External account binding is not supported for ACME v1') raise ModuleFailException(
url = self.client.directory['new-reg'] "External account binding is not supported for ACME v1"
)
url = self.client.directory["new-reg"]
else: else:
if (external_account_binding is not None or self.client.directory['meta'].get('externalAccountRequired')) and allow_creation: if (
external_account_binding is not None
or self.client.directory["meta"].get("externalAccountRequired")
) and allow_creation:
# Some ACME servers such as ZeroSSL do not like it when you try to register an existing account # Some ACME servers such as ZeroSSL do not like it when you try to register an existing account
# and provide external_account_binding credentials. Thus we first send a request with allow_creation=False # and provide external_account_binding credentials. Thus we first send a request with allow_creation=False
# to see whether the account already exists. # to see whether the account already exists.
# Unfortunately, for other ACME servers it's the other way around: (at least some) HARICA endpoints
# do not allow *any* access without external account data. That's why we catch errors and check
# for 'externalAccountRequired'.
try:
# Note that we pass contact here: ZeroSSL does not accept registration calls without contacts, even # Note that we pass contact here: ZeroSSL does not accept registration calls without contacts, even
# if onlyReturnExisting is set to true. # if onlyReturnExisting is set to true.
created, data = self._new_reg(contact=contact, allow_creation=False) created, data = self._new_reg(contact=contact, allow_creation=False)
@@ -71,45 +84,65 @@ class ACMEAccount(object):
# An account already exists! Return data # An account already exists! Return data
return created, data return created, data
# An account does not yet exist. Try to create one next. # An account does not yet exist. Try to create one next.
except ACMEProtocolException as exc:
if (
exc.error_type
!= "urn:ietf:params:acme:error:externalAccountRequired"
or external_account_binding is None
):
# Either another error happened, or we got 'externalAccountRequired' and external account data was not supplied
# => re-raise exception!
raise
# In this case, the server really wants external account data.
# The below code tries to create the account with external account data present.
new_reg = { new_reg = {"contact": contact}
'contact': contact
}
if not allow_creation: if not allow_creation:
# https://tools.ietf.org/html/rfc8555#section-7.3.1 # https://tools.ietf.org/html/rfc8555#section-7.3.1
new_reg['onlyReturnExisting'] = True new_reg["onlyReturnExisting"] = True
if terms_agreed: if terms_agreed:
new_reg['termsOfServiceAgreed'] = True new_reg["termsOfServiceAgreed"] = True
url = self.client.directory['newAccount'] url = self.client.directory["newAccount"]
if external_account_binding is not None: if external_account_binding is not None:
new_reg['externalAccountBinding'] = self.client.sign_request( new_reg["externalAccountBinding"] = self.client.sign_request(
{ {
'alg': external_account_binding['alg'], "alg": external_account_binding["alg"],
'kid': external_account_binding['kid'], "kid": external_account_binding["kid"],
'url': url, "url": url,
}, },
self.client.account_jwk, self.client.account_jwk,
self.client.backend.create_mac_key(external_account_binding['alg'], external_account_binding['key']) self.client.backend.create_mac_key(
external_account_binding["alg"], external_account_binding["key"]
),
) )
elif self.client.directory['meta'].get('externalAccountRequired') and allow_creation: elif (
self.client.directory["meta"].get("externalAccountRequired")
and allow_creation
):
raise ModuleFailException( raise ModuleFailException(
'To create an account, an external account binding must be specified. ' "To create an account, an external account binding must be specified. "
'Use the acme_account module with the external_account_binding option.' "Use the acme_account module with the external_account_binding option."
) )
result, info = self.client.send_signed_request(url, new_reg, fail_on_error=False) result, info = self.client.send_signed_request(
url, new_reg, fail_on_error=False
)
if not isinstance(result, Mapping): if not isinstance(result, Mapping):
raise ACMEProtocolException( raise ACMEProtocolException(
self.client.module, msg='Invalid account creation reply from ACME server', info=info, content=result) self.client.module,
msg="Invalid account creation reply from ACME server",
info=info,
content=result,
)
if info['status'] in ([200, 201] if self.client.version == 1 else [201]): if info["status"] in ([200, 201] if self.client.version == 1 else [201]):
# Account did not exist # Account did not exist
if 'location' in info: if "location" in info:
self.client.set_account_uri(info['location']) self.client.set_account_uri(info["location"])
return True, result return True, result
elif info['status'] == (409 if self.client.version == 1 else 200): elif info["status"] == (409 if self.client.version == 1 else 200):
# Account did exist # Account did exist
if result.get('status') == 'deactivated': if result.get("status") == "deactivated":
# A bug in Pebble (https://github.com/letsencrypt/pebble/issues/179) and # A bug in Pebble (https://github.com/letsencrypt/pebble/issues/179) and
# Boulder (https://github.com/letsencrypt/boulder/issues/3971): this should # Boulder (https://github.com/letsencrypt/boulder/issues/3971): this should
# not return a valid account object according to # not return a valid account object according to
@@ -120,15 +153,23 @@ class ACMEAccount(object):
return False, None return False, None
else: else:
raise ModuleFailException("Account is deactivated") raise ModuleFailException("Account is deactivated")
if 'location' in info: if "location" in info:
self.client.set_account_uri(info['location']) self.client.set_account_uri(info["location"])
return False, result return False, result
elif info['status'] in (400, 404) and result['type'] == 'urn:ietf:params:acme:error:accountDoesNotExist' and not allow_creation: elif (
info["status"] in (400, 404)
and result["type"] == "urn:ietf:params:acme:error:accountDoesNotExist"
and not allow_creation
):
# Account does not exist (and we did not try to create it) # Account does not exist (and we did not try to create it)
# (According to RFC 8555, Section 7.3.1, the HTTP status code MUST be 400. # (According to RFC 8555, Section 7.3.1, the HTTP status code MUST be 400.
# Unfortunately Digicert does not care and sends 404 instead.) # Unfortunately Digicert does not care and sends 404 instead.)
return False, None return False, None
elif info['status'] == 403 and result['type'] == 'urn:ietf:params:acme:error:unauthorized' and 'deactivated' in (result.get('detail') or ''): elif (
info["status"] == 403
and result["type"] == "urn:ietf:params:acme:error:unauthorized"
and "deactivated" in (result.get("detail") or "")
):
# Account has been deactivated; currently works for Pebble; has not been # Account has been deactivated; currently works for Pebble; has not been
# implemented for Boulder (https://github.com/letsencrypt/boulder/issues/3971), # implemented for Boulder (https://github.com/letsencrypt/boulder/issues/3971),
# might need adjustment in error detection. # might need adjustment in error detection.
@@ -138,47 +179,80 @@ class ACMEAccount(object):
raise ModuleFailException("Account is deactivated") raise ModuleFailException("Account is deactivated")
else: else:
raise ACMEProtocolException( raise ACMEProtocolException(
self.client.module, msg='Registering ACME account failed', info=info, content_json=result) self.client.module,
msg="Registering ACME account failed",
info=info,
content_json=result,
)
def get_account_data(self): def get_account_data(self):
''' """
Retrieve account information. Can only be called when the account Retrieve account information. Can only be called when the account
URI is already known (such as after calling setup_account). URI is already known (such as after calling setup_account).
Return None if the account was deactivated, or a dict otherwise. Return None if the account was deactivated, or a dict otherwise.
''' """
if self.client.account_uri is None: if self.client.account_uri is None:
raise ModuleFailException("Account URI unknown") raise ModuleFailException("Account URI unknown")
if self.client.version == 1: if self.client.version == 1:
data = {} data = {}
data['resource'] = 'reg' data["resource"] = "reg"
result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False) result, info = self.client.send_signed_request(
self.client.account_uri, data, fail_on_error=False
)
else: else:
# try POST-as-GET first (draft-15 or newer) # try POST-as-GET first (draft-15 or newer)
data = None data = None
result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False) result, info = self.client.send_signed_request(
self.client.account_uri, data, fail_on_error=False
)
# check whether that failed with a malformed request error # check whether that failed with a malformed request error
if info['status'] >= 400 and result.get('type') == 'urn:ietf:params:acme:error:malformed': if (
info["status"] >= 400
and result.get("type") == "urn:ietf:params:acme:error:malformed"
):
# retry as a regular POST (with no changed data) for pre-draft-15 ACME servers # retry as a regular POST (with no changed data) for pre-draft-15 ACME servers
data = {} data = {}
result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False) result, info = self.client.send_signed_request(
self.client.account_uri, data, fail_on_error=False
)
if not isinstance(result, Mapping): if not isinstance(result, Mapping):
raise ACMEProtocolException( raise ACMEProtocolException(
self.client.module, msg='Invalid account data retrieved from ACME server', info=info, content=result) self.client.module,
if info['status'] in (400, 403) and result.get('type') == 'urn:ietf:params:acme:error:unauthorized': msg="Invalid account data retrieved from ACME server",
info=info,
content=result,
)
if (
info["status"] in (400, 403)
and result.get("type") == "urn:ietf:params:acme:error:unauthorized"
):
# Returned when account is deactivated # Returned when account is deactivated
return None return None
if info['status'] in (400, 404) and result.get('type') == 'urn:ietf:params:acme:error:accountDoesNotExist': if (
info["status"] in (400, 404)
and result.get("type") == "urn:ietf:params:acme:error:accountDoesNotExist"
):
# Returned when account does not exist # Returned when account does not exist
return None return None
if info['status'] < 200 or info['status'] >= 300: if info["status"] < 200 or info["status"] >= 300:
raise ACMEProtocolException( raise ACMEProtocolException(
self.client.module, msg='Error retrieving account data', info=info, content_json=result) self.client.module,
msg="Error retrieving account data",
info=info,
content_json=result,
)
return result return result
def setup_account(self, contact=None, agreement=None, terms_agreed=False, def setup_account(
allow_creation=True, remove_account_uri_if_not_exists=False, self,
external_account_binding=None): contact=None,
''' agreement=None,
terms_agreed=False,
allow_creation=True,
remove_account_uri_if_not_exists=False,
external_account_binding=None,
):
"""
Detect or create an account on the ACME server. For ACME v1, Detect or create an account on the ACME server. For ACME v1,
as the only way (without knowing an account URI) to test if an as the only way (without knowing an account URI) to test if an
account exists is to try and create one with the provided account account exists is to try and create one with the provided account
@@ -202,7 +276,7 @@ class ACMEAccount(object):
(https://tools.ietf.org/html/rfc8555#section-7.3.4). (https://tools.ietf.org/html/rfc8555#section-7.3.4).
https://tools.ietf.org/html/rfc8555#section-7.3 https://tools.ietf.org/html/rfc8555#section-7.3
''' """
if self.client.account_uri is not None: if self.client.account_uri is not None:
created = False created = False
@@ -213,7 +287,9 @@ class ACMEAccount(object):
if remove_account_uri_if_not_exists and not allow_creation: if remove_account_uri_if_not_exists and not allow_creation:
self.client.account_uri = None self.client.account_uri = None
else: else:
raise ModuleFailException("Account is deactivated or does not exist!") raise ModuleFailException(
"Account is deactivated or does not exist!"
)
else: else:
created, account_data = self._new_reg( created, account_data = self._new_reg(
contact, contact,
@@ -222,15 +298,17 @@ class ACMEAccount(object):
allow_creation=allow_creation and not self.client.module.check_mode, allow_creation=allow_creation and not self.client.module.check_mode,
external_account_binding=external_account_binding, external_account_binding=external_account_binding,
) )
if self.client.module.check_mode and self.client.account_uri is None and allow_creation: if (
self.client.module.check_mode
and self.client.account_uri is None
and allow_creation
):
created = True created = True
account_data = { account_data = {"contact": contact or []}
'contact': contact or []
}
return created, account_data return created, account_data
def update_account(self, account_data, contact=None): def update_account(self, account_data, contact=None):
''' """
Update an account on the ACME server. Check mode is fully respected. Update an account on the ACME server. Check mode is fully respected.
The current account data must be provided as ``account_data``. The current account data must be provided as ``account_data``.
@@ -241,11 +319,11 @@ class ACMEAccount(object):
account data. account data.
https://tools.ietf.org/html/rfc8555#section-7.3.2 https://tools.ietf.org/html/rfc8555#section-7.3.2
''' """
# Create request # Create request
update_request = {} update_request = {}
if contact is not None and account_data.get('contact', []) != contact: if contact is not None and account_data.get("contact", []) != contact:
update_request['contact'] = list(contact) update_request["contact"] = list(contact)
# No change? # No change?
if not update_request: if not update_request:
@@ -257,10 +335,16 @@ class ACMEAccount(object):
account_data.update(update_request) account_data.update(update_request)
else: else:
if self.client.version == 1: if self.client.version == 1:
update_request['resource'] = 'reg' update_request["resource"] = "reg"
account_data, info = self.client.send_signed_request(self.client.account_uri, update_request) account_data, info = self.client.send_signed_request(
self.client.account_uri, update_request
)
if not isinstance(account_data, Mapping): if not isinstance(account_data, Mapping):
raise ACMEProtocolException( raise ACMEProtocolException(
self.client.module, msg='Invalid account updating reply from ACME server', info=info, content=account_data) self.client.module,
msg="Invalid account updating reply from ACME server",
info=info,
content=account_data,
)
return True, account_data return True, account_data

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -18,36 +20,34 @@ import traceback
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.common.text.converters import to_bytes
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.six import PY3 from ansible.module_utils.six import PY3
from ansible.module_utils.urls import fetch_url
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
OpenSSLCLIBackend,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import ( from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import (
CryptographyBackend,
CRYPTOGRAPHY_ERROR, CRYPTOGRAPHY_ERROR,
CRYPTOGRAPHY_MINIMAL_VERSION, CRYPTOGRAPHY_MINIMAL_VERSION,
CRYPTOGRAPHY_VERSION, CRYPTOGRAPHY_VERSION,
HAS_CURRENT_CRYPTOGRAPHY, HAS_CURRENT_CRYPTOGRAPHY,
CryptographyBackend,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
OpenSSLCLIBackend,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ACMEProtocolException, ACMEProtocolException,
NetworkException,
ModuleFailException,
KeyParsingError, KeyParsingError,
ModuleFailException,
NetworkException,
format_http_status, format_http_status,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
compute_cert_id, compute_cert_id,
nopad_b64, nopad_b64,
parse_retry_after, parse_retry_after,
) )
from ansible_collections.community.crypto.plugins.module_utils.argspec import (
ArgumentSpec,
)
try: try:
import ipaddress # noqa: F401, pylint: disable=unused-import import ipaddress # noqa: F401, pylint: disable=unused-import
@@ -66,72 +66,97 @@ RETRY_COUNT = 10
def _decode_retry(module, response, info, retry_count): def _decode_retry(module, response, info, retry_count):
if info['status'] not in RETRY_STATUS_CODES: if info["status"] not in RETRY_STATUS_CODES:
return False return False
if retry_count >= RETRY_COUNT: if retry_count >= RETRY_COUNT:
raise ACMEProtocolException( raise ACMEProtocolException(
module, msg='Giving up after {retry} retries'.format(retry=RETRY_COUNT), info=info, response=response) module,
msg="Giving up after {retry} retries".format(retry=RETRY_COUNT),
info=info,
response=response,
)
# 429 and 503 should have a Retry-After header (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) # 429 and 503 should have a Retry-After header (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
try: try:
retry_after = min(max(1, int(info.get('retry-after'))), 60) retry_after = min(max(1, int(info.get("retry-after"))), 60)
except (TypeError, ValueError) as dummy: except (TypeError, ValueError):
retry_after = 10 retry_after = 10
module.log('Retrieved a %s HTTP status on %s, retrying in %s seconds' % (format_http_status(info['status']), info['url'], retry_after)) module.log(
"Retrieved a %s HTTP status on %s, retrying in %s seconds"
% (format_http_status(info["status"]), info["url"], retry_after)
)
time.sleep(retry_after) time.sleep(retry_after)
return True return True
def _assert_fetch_url_success(module, response, info, allow_redirect=False, allow_client_error=True, allow_server_error=True): def _assert_fetch_url_success(
if info['status'] < 0: module,
raise NetworkException(msg="Failure downloading %s, %s" % (info['url'], info['msg'])) response,
info,
allow_redirect=False,
allow_client_error=True,
allow_server_error=True,
):
if info["status"] < 0:
raise NetworkException(
msg="Failure downloading %s, %s" % (info["url"], info["msg"])
)
if (300 <= info['status'] < 400 and not allow_redirect) or \ if (
(400 <= info['status'] < 500 and not allow_client_error) or \ (300 <= info["status"] < 400 and not allow_redirect)
(info['status'] >= 500 and not allow_server_error): or (400 <= info["status"] < 500 and not allow_client_error)
or (info["status"] >= 500 and not allow_server_error)
):
raise ACMEProtocolException(module, info=info, response=response) raise ACMEProtocolException(module, info=info, response=response)
def _is_failed(info, expected_status_codes=None): def _is_failed(info, expected_status_codes=None):
if info['status'] < 200 or info['status'] >= 400: if info["status"] < 200 or info["status"] >= 400:
return True return True
if expected_status_codes is not None and info['status'] not in expected_status_codes: if (
expected_status_codes is not None
and info["status"] not in expected_status_codes
):
return True return True
return False return False
class ACMEDirectory(object): class ACMEDirectory(object):
''' """
The ACME server directory. Gives access to the available resources, The ACME server directory. Gives access to the available resources,
and allows to obtain a Replay-Nonce. The acme_directory URL and allows to obtain a Replay-Nonce. The acme_directory URL
needs to support unauthenticated GET requests; ACME endpoints needs to support unauthenticated GET requests; ACME endpoints
requiring authentication are not supported. requiring authentication are not supported.
https://tools.ietf.org/html/rfc8555#section-7.1.1 https://tools.ietf.org/html/rfc8555#section-7.1.1
''' """
def __init__(self, module, account): def __init__(self, module, account):
self.module = module self.module = module
self.directory_root = module.params['acme_directory'] self.directory_root = module.params["acme_directory"]
self.version = module.params['acme_version'] self.version = module.params["acme_version"]
self.directory, dummy = account.get_request(self.directory_root, get_only=True) self.directory, dummy = account.get_request(self.directory_root, get_only=True)
self.request_timeout = module.params['request_timeout'] self.request_timeout = module.params["request_timeout"]
# Check whether self.version matches what we expect # Check whether self.version matches what we expect
if self.version == 1: if self.version == 1:
for key in ('new-reg', 'new-authz', 'new-cert'): for key in ("new-reg", "new-authz", "new-cert"):
if key not in self.directory: if key not in self.directory:
raise ModuleFailException("ACME directory does not seem to follow protocol ACME v1") raise ModuleFailException(
"ACME directory does not seem to follow protocol ACME v1"
)
if self.version == 2: if self.version == 2:
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 # Make sure that 'meta' is always available
if 'meta' not in self.directory: if "meta" not in self.directory:
self.directory['meta'] = {} self.directory["meta"] = {}
def __getitem__(self, key): def __getitem__(self, key):
return self.directory[key] return self.directory[key]
@@ -143,35 +168,48 @@ class ACMEDirectory(object):
return self.directory.get(key, default_value) return self.directory.get(key, default_value)
def get_nonce(self, resource=None): def get_nonce(self, resource=None):
url = self.directory_root if self.version == 1 else self.directory['newNonce'] url = self.directory_root if self.version == 1 else self.directory["newNonce"]
if resource is not None: if resource is not None:
url = resource url = resource
retry_count = 0 retry_count = 0
while True: while True:
response, info = fetch_url(self.module, url, method='HEAD', timeout=self.request_timeout) response, info = fetch_url(
self.module, url, method="HEAD", timeout=self.request_timeout
)
if _decode_retry(self.module, response, info, retry_count): if _decode_retry(self.module, response, info, retry_count):
retry_count += 1 retry_count += 1
continue continue
if info['status'] not in (200, 204): if info["status"] not in (200, 204):
raise NetworkException("Failed to get replay-nonce, got status {0}".format(format_http_status(info['status']))) raise NetworkException(
if 'replay-nonce' in info: "Failed to get replay-nonce, got status {0}".format(
return info['replay-nonce'] format_http_status(info["status"])
)
)
if "replay-nonce" in info:
return info["replay-nonce"]
self.module.log( self.module.log(
'HEAD to {0} did return status {1}, but no replay-nonce header!'.format(url, format_http_status(info['status']))) "HEAD to {0} did return status {1}, but no replay-nonce header!".format(
url, format_http_status(info["status"])
)
)
if retry_count >= 5: if retry_count >= 5:
raise ACMEProtocolException( raise ACMEProtocolException(
self.module, msg='Was not able to obtain nonce, giving up after 5 retries', info=info, response=response) self.module,
msg="Was not able to obtain nonce, giving up after 5 retries",
info=info,
response=response,
)
retry_count += 1 retry_count += 1
def has_renewal_info_endpoint(self): def has_renewal_info_endpoint(self):
return 'renewalInfo' in self.directory return "renewalInfo" in self.directory
class ACMEClient(object): class ACMEClient(object):
''' """
ACME client object. Handles the authorized communication with the ACME client object. Handles the authorized communication with the
ACME server. ACME server.
''' """
def __init__(self, module, backend): def __init__(self, module, backend):
# Set to true to enable logging of all signed requests # Set to true to enable logging of all signed requests
@@ -179,17 +217,17 @@ class ACMEClient(object):
self.module = module self.module = module
self.backend = backend self.backend = backend
self.version = module.params['acme_version'] self.version = module.params["acme_version"]
# account_key path and content are mutually exclusive # account_key path and content are mutually exclusive
self.account_key_file = module.params.get('account_key_src') self.account_key_file = module.params.get("account_key_src")
self.account_key_content = module.params.get('account_key_content') self.account_key_content = module.params.get("account_key_content")
self.account_key_passphrase = module.params.get('account_key_passphrase') self.account_key_passphrase = module.params.get("account_key_passphrase")
# Grab account URI from module parameters. # Grab account URI from module parameters.
# Make sure empty string is treated as None. # Make sure empty string is treated as None.
self.account_uri = module.params.get('account_uri') or None self.account_uri = module.params.get("account_uri") or None
self.request_timeout = module.params['request_timeout'] self.request_timeout = module.params["request_timeout"]
self.account_key_data = None self.account_key_data = None
self.account_jwk = None self.account_jwk = None
@@ -199,12 +237,15 @@ class ACMEClient(object):
self.account_key_data = self.parse_key( self.account_key_data = self.parse_key(
key_file=self.account_key_file, key_file=self.account_key_file,
key_content=self.account_key_content, key_content=self.account_key_content,
passphrase=self.account_key_passphrase) passphrase=self.account_key_passphrase,
)
except KeyParsingError as e: except KeyParsingError as e:
raise ModuleFailException("Error while parsing account key: {msg}".format(msg=e.msg)) raise ModuleFailException(
self.account_jwk = self.account_key_data['jwk'] "Error while parsing account key: {msg}".format(msg=e.msg)
)
self.account_jwk = self.account_key_data["jwk"]
self.account_jws_header = { self.account_jws_header = {
"alg": self.account_key_data['alg'], "alg": self.account_key_data["alg"],
"jwk": self.account_jwk, "jwk": self.account_jwk,
} }
if self.account_uri: if self.account_uri:
@@ -214,56 +255,76 @@ class ACMEClient(object):
self.directory = ACMEDirectory(module, self) self.directory = ACMEDirectory(module, self)
def set_account_uri(self, uri): def set_account_uri(self, uri):
''' """
Set account URI. For ACME v2, it needs to be used to sending signed Set account URI. For ACME v2, it needs to be used to sending signed
requests. requests.
''' """
self.account_uri = uri self.account_uri = uri
if self.version != 1: if self.version != 1:
self.account_jws_header.pop('jwk') self.account_jws_header.pop("jwk")
self.account_jws_header['kid'] = self.account_uri self.account_jws_header["kid"] = self.account_uri
def parse_key(self, key_file=None, key_content=None, passphrase=None): def parse_key(self, key_file=None, key_content=None, passphrase=None):
''' """
Parses an RSA or Elliptic Curve key file in PEM format and returns key_data. Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
In case of an error, raises KeyParsingError. In case of an error, raises KeyParsingError.
''' """
if key_file is None and key_content is None: if key_file is None and key_content is None:
raise AssertionError('One of key_file and key_content must be specified!') raise AssertionError("One of key_file and key_content must be specified!")
return self.backend.parse_key(key_file, key_content, passphrase=passphrase) return self.backend.parse_key(key_file, key_content, passphrase=passphrase)
def sign_request(self, protected, payload, key_data, encode_payload=True): def sign_request(self, protected, payload, key_data, encode_payload=True):
''' """
Signs an ACME request. Signs an ACME request.
''' """
try: try:
if payload is None: if payload is None:
# POST-as-GET # POST-as-GET
payload64 = '' payload64 = ""
else: else:
# POST # POST
if encode_payload: if encode_payload:
payload = self.module.jsonify(payload).encode('utf8') payload = self.module.jsonify(payload).encode("utf8")
payload64 = nopad_b64(to_bytes(payload)) payload64 = nopad_b64(to_bytes(payload))
protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8')) protected64 = nopad_b64(self.module.jsonify(protected).encode("utf8"))
except Exception as e: except Exception as e:
raise ModuleFailException("Failed to encode payload / headers as JSON: {0}".format(e)) raise ModuleFailException(
"Failed to encode payload / headers as JSON: {0}".format(e)
)
return self.backend.sign(payload64, protected64, key_data) return self.backend.sign(payload64, protected64, key_data)
def _log(self, msg, data=None): def _log(self, msg, data=None):
''' """
Write arguments to acme.log when logging is enabled. Write arguments to acme.log when logging is enabled.
''' """
if self._debug: if self._debug:
with open('acme.log', 'ab') as f: with open("acme.log", "ab") as f:
f.write('[{0}] {1}\n'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%s'), msg).encode('utf-8')) f.write(
"[{0}] {1}\n".format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%s"), msg
).encode("utf-8")
)
if data is not None: if data is not None:
f.write('{0}\n\n'.format(json.dumps(data, indent=2, sort_keys=True)).encode('utf-8')) f.write(
"{0}\n\n".format(
json.dumps(data, indent=2, sort_keys=True)
).encode("utf-8")
)
def send_signed_request(self, url, payload, key_data=None, jws_header=None, parse_json_result=True, def send_signed_request(
encode_payload=True, fail_on_error=True, error_msg=None, expected_status_codes=None): self,
''' url,
payload,
key_data=None,
jws_header=None,
parse_json_result=True,
encode_payload=True,
fail_on_error=True,
error_msg=None,
expected_status_codes=None,
):
"""
Sends a JWS signed HTTP POST request to the ACME server and returns Sends a JWS signed HTTP POST request to the ACME server and returns
the response as dictionary (if parse_json_result is True) or in raw form the response as dictionary (if parse_json_result is True) or in raw form
(if parse_json_result is False). (if parse_json_result is False).
@@ -271,7 +332,7 @@ class ACMEClient(object):
If payload is None, a POST-as-GET is performed. If payload is None, a POST-as-GET is performed.
(https://tools.ietf.org/html/rfc8555#section-6.3) (https://tools.ietf.org/html/rfc8555#section-6.3)
''' """
key_data = key_data or self.account_key_data key_data = key_data or self.account_key_data
jws_header = jws_header or self.account_jws_header jws_header = jws_header or self.account_jws_header
failed_tries = 0 failed_tries = 0
@@ -281,21 +342,30 @@ class ACMEClient(object):
if self.version != 1: if self.version != 1:
protected["url"] = url protected["url"] = url
self._log('URL', url) self._log("URL", url)
self._log('protected', protected) self._log("protected", protected)
self._log('payload', payload) self._log("payload", payload)
data = self.sign_request(protected, payload, key_data, encode_payload=encode_payload) data = self.sign_request(
protected, payload, key_data, encode_payload=encode_payload
)
if self.version == 1: if self.version == 1:
data["header"] = jws_header.copy() data["header"] = jws_header.copy()
for k, v in protected.items(): for k, v in protected.items():
dummy = data["header"].pop(k, None) data["header"].pop(k, None)
self._log('signed request', data) self._log("signed request", data)
data = self.module.jsonify(data) data = self.module.jsonify(data)
headers = { headers = {
'Content-Type': 'application/jose+json', "Content-Type": "application/jose+json",
} }
resp, info = fetch_url(self.module, url, data=data, headers=headers, method='POST', timeout=self.request_timeout) resp, info = fetch_url(
self.module,
url,
data=data,
headers=headers,
method="POST",
timeout=self.request_timeout,
)
if _decode_retry(self.module, resp, info, failed_tries): if _decode_retry(self.module, resp, info, failed_tries):
failed_tries += 1 failed_tries += 1
continue continue
@@ -309,20 +379,26 @@ class ACMEClient(object):
raise TypeError raise TypeError
content = resp.read() content = resp.read()
except (AttributeError, TypeError): 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:
if (parse_json_result and info['content-type'].startswith('application/json')) or 400 <= info['status'] < 600: if (
parse_json_result
and info["content-type"].startswith("application/json")
) or 400 <= info["status"] < 600:
try: try:
decoded_result = self.module.from_json(content.decode('utf8')) decoded_result = self.module.from_json(content.decode("utf8"))
self._log('parsed result', decoded_result) self._log("parsed result", decoded_result)
# In case of badNonce error, try again (up to 5 times) # In case of badNonce error, try again (up to 5 times)
# (https://tools.ietf.org/html/rfc8555#section-6.7) # (https://tools.ietf.org/html/rfc8555#section-6.7)
if all(( if all(
400 <= info['status'] < 600, (
decoded_result.get('type') == 'urn:ietf:params:acme:error:badNonce', 400 <= info["status"] < 600,
decoded_result.get("type")
== "urn:ietf:params:acme:error:badNonce",
failed_tries <= 5, failed_tries <= 5,
)): )
):
failed_tries += 1 failed_tries += 1
continue continue
if parse_json_result: if parse_json_result:
@@ -330,25 +406,46 @@ class ACMEClient(object):
else: else:
result = content result = content
except ValueError: except ValueError:
raise NetworkException("Failed to parse the ACME response: {0} {1}".format(url, content)) raise NetworkException(
"Failed to parse the ACME response: {0} {1}".format(
url, content
)
)
else: else:
result = content result = content
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 parse_json_result else None,
)
return result, info return result, info
def get_request(self, uri, parse_json_result=True, headers=None, get_only=False, def get_request(
fail_on_error=True, error_msg=None, expected_status_codes=None): self,
''' uri,
parse_json_result=True,
headers=None,
get_only=False,
fail_on_error=True,
error_msg=None,
expected_status_codes=None,
):
"""
Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback
to GET if server replies with a status code of 405. to GET if server replies with a status code of 405.
''' """
if not get_only and self.version != 1: if not get_only and self.version != 1:
# Try POST-as-GET # Try POST-as-GET
content, info = self.send_signed_request(uri, None, parse_json_result=False, fail_on_error=False) content, info = self.send_signed_request(
if info['status'] == 405: uri, None, parse_json_result=False, fail_on_error=False
)
if info["status"] == 405:
# Instead, do unauthenticated GET # Instead, do unauthenticated GET
get_only = True get_only = True
else: else:
@@ -359,7 +456,13 @@ class ACMEClient(object):
# Perform unauthenticated GET # Perform unauthenticated GET
retry_count = 0 retry_count = 0
while True: while True:
resp, info = fetch_url(self.module, uri, method='GET', headers=headers, timeout=self.request_timeout) resp, info = fetch_url(
self.module,
uri,
method="GET",
headers=headers,
timeout=self.request_timeout,
)
if not _decode_retry(self.module, resp, info, retry_count): if not _decode_retry(self.module, resp, info, retry_count):
break break
retry_count += 1 retry_count += 1
@@ -373,27 +476,38 @@ class ACMEClient(object):
raise TypeError raise TypeError
content = resp.read() content = resp.read()
except (AttributeError, TypeError): except (AttributeError, TypeError):
content = info.pop('body', None) content = info.pop("body", None)
# Process result # Process result
parsed_json_result = False 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 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:
result = content result = content
else: else:
result = content result = content
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 parsed_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
def get_renewal_info( def get_renewal_info(
@@ -406,19 +520,30 @@ class ACMEClient(object):
retry_after_relative_with_timezone=True, retry_after_relative_with_timezone=True,
): ):
if not self.directory.has_renewal_info_endpoint(): if not self.directory.has_renewal_info_endpoint():
raise ModuleFailException('The ACME endpoint does not support ACME Renewal Information retrieval') raise ModuleFailException(
"The ACME endpoint does not support ACME Renewal Information retrieval"
)
if cert_id is None: if cert_id is None:
cert_id = compute_cert_id(self.backend, cert_info=cert_info, cert_filename=cert_filename, cert_content=cert_content) cert_id = compute_cert_id(
url = '{base}/{cert_id}'.format(base=self.directory.directory['renewalInfo'].rstrip('/'), cert_id=cert_id) self.backend,
cert_info=cert_info,
cert_filename=cert_filename,
cert_content=cert_content,
)
url = "{base}/{cert_id}".format(
base=self.directory.directory["renewalInfo"].rstrip("/"), cert_id=cert_id
)
data, info = self.get_request(url, parse_json_result=True, fail_on_error=True, get_only=True) data, info = self.get_request(
url, parse_json_result=True, fail_on_error=True, get_only=True
)
# Include Retry-After header if asked for # Include Retry-After header if asked for
if include_retry_after and 'retry-after' in info: if include_retry_after and "retry-after" in info:
try: try:
data['retryAfter'] = parse_retry_after( data["retryAfter"] = parse_retry_after(
info['retry-after'], info["retry-after"],
relative_with_timezone=retry_after_relative_with_timezone, relative_with_timezone=retry_after_relative_with_timezone,
) )
except ValueError: except ValueError:
@@ -427,21 +552,23 @@ class ACMEClient(object):
def get_default_argspec(): def get_default_argspec():
''' """
Provides default argument spec for the options documented in the acme doc fragment. Provides default argument spec for the options documented in the acme doc fragment.
DEPRECATED: will be removed in community.crypto 3.0.0 DEPRECATED: will be removed in community.crypto 3.0.0
''' """
return dict( return dict(
acme_directory=dict(type='str', required=True), acme_directory=dict(type="str", required=True),
acme_version=dict(type='int', required=True, choices=[1, 2]), acme_version=dict(type="int", required=True, choices=[1, 2]),
validate_certs=dict(type='bool', default=True), validate_certs=dict(type="bool", default=True),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']), select_crypto_backend=dict(
request_timeout=dict(type='int', default=10), type="str", default="auto", choices=["auto", "openssl", "cryptography"]
account_key_src=dict(type='path', aliases=['account_key']), ),
account_key_content=dict(type='str', no_log=True), request_timeout=dict(type="int", default=10),
account_key_passphrase=dict(type='str', no_log=True), account_key_src=dict(type="path", aliases=["account_key"]),
account_uri=dict(type='str'), account_key_content=dict(type="str", no_log=True),
account_key_passphrase=dict(type="str", no_log=True),
account_uri=dict(type="str"),
) )
@@ -450,90 +577,109 @@ def create_default_argspec(
require_account_key=True, require_account_key=True,
with_certificate=False, with_certificate=False,
): ):
''' """
Provides default argument spec for the options documented in the acme doc fragment. Provides default argument spec for the options documented in the acme doc fragment.
''' """
result = ArgumentSpec( result = ArgumentSpec(
argument_spec=dict( argument_spec=dict(
acme_directory=dict(type='str', required=True), acme_directory=dict(type="str", required=True),
acme_version=dict(type='int', required=True, choices=[1, 2]), acme_version=dict(type="int", required=True, choices=[1, 2]),
validate_certs=dict(type='bool', default=True), validate_certs=dict(type="bool", default=True),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']), select_crypto_backend=dict(
request_timeout=dict(type='int', default=10), type="str", default="auto", choices=["auto", "openssl", "cryptography"]
),
request_timeout=dict(type="int", default=10),
), ),
) )
if with_account: if with_account:
result.update_argspec( result.update_argspec(
account_key_src=dict(type='path', aliases=['account_key']), account_key_src=dict(type="path", aliases=["account_key"]),
account_key_content=dict(type='str', no_log=True), account_key_content=dict(type="str", no_log=True),
account_key_passphrase=dict(type='str', no_log=True), account_key_passphrase=dict(type="str", no_log=True),
account_uri=dict(type='str'), account_uri=dict(type="str"),
) )
if require_account_key: if require_account_key:
result.update(required_one_of=[['account_key_src', 'account_key_content']]) result.update(required_one_of=[["account_key_src", "account_key_content"]])
result.update(mutually_exclusive=[['account_key_src', 'account_key_content']]) result.update(mutually_exclusive=[["account_key_src", "account_key_content"]])
if with_certificate: if with_certificate:
result.update_argspec( result.update_argspec(
csr=dict(type='path'), csr=dict(type="path"),
csr_content=dict(type='str'), csr_content=dict(type="str"),
) )
result.update( result.update(
required_one_of=[['csr', 'csr_content']], required_one_of=[["csr", "csr_content"]],
mutually_exclusive=[['csr', 'csr_content']], mutually_exclusive=[["csr", "csr_content"]],
) )
return result return result
def create_backend(module, needs_acme_v2): def create_backend(module, needs_acme_v2):
if not HAS_IPADDRESS: if not HAS_IPADDRESS:
module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMPORT_ERROR) module.fail_json(
msg=missing_required_lib("ipaddress"), exception=IPADDRESS_IMPORT_ERROR
)
backend = module.params['select_crypto_backend'] backend = module.params["select_crypto_backend"]
# Backend autodetect # Backend autodetect
if backend == 'auto': if backend == "auto":
backend = 'cryptography' if HAS_CURRENT_CRYPTOGRAPHY else 'openssl' backend = "cryptography" if HAS_CURRENT_CRYPTOGRAPHY else "openssl"
# Create backend object # Create backend object
if backend == 'cryptography': if backend == "cryptography":
if CRYPTOGRAPHY_ERROR is not None: if CRYPTOGRAPHY_ERROR is not None:
# Either we could not import cryptography at all, or there was an unexpected error # Either we could not import cryptography at all, or there was an unexpected error
if CRYPTOGRAPHY_VERSION is None: if CRYPTOGRAPHY_VERSION is None:
msg = missing_required_lib('cryptography') msg = missing_required_lib("cryptography")
else: else:
msg = 'Unexpected error while preparing cryptography: {0}'.format(CRYPTOGRAPHY_ERROR.splitlines()[-1]) msg = "Unexpected error while preparing cryptography: {0}".format(
CRYPTOGRAPHY_ERROR.splitlines()[-1]
)
module.fail_json(msg=msg, exception=CRYPTOGRAPHY_ERROR) module.fail_json(msg=msg, exception=CRYPTOGRAPHY_ERROR)
if not HAS_CURRENT_CRYPTOGRAPHY: if not HAS_CURRENT_CRYPTOGRAPHY:
# We succeeded importing cryptography, but its version is too old. # We succeeded importing cryptography, but its version is too old.
module.fail_json( module.fail_json(
msg='Found cryptography, but only version {0}. {1}'.format( msg="Found cryptography, but only version {0}. {1}".format(
CRYPTOGRAPHY_VERSION, CRYPTOGRAPHY_VERSION,
missing_required_lib('cryptography >= {0}'.format(CRYPTOGRAPHY_MINIMAL_VERSION)))) missing_required_lib(
module.debug('Using cryptography backend (library version {0})'.format(CRYPTOGRAPHY_VERSION)) "cryptography >= {0}".format(CRYPTOGRAPHY_MINIMAL_VERSION)
),
)
)
module.debug(
"Using cryptography backend (library version {0})".format(
CRYPTOGRAPHY_VERSION
)
)
module_backend = CryptographyBackend(module) module_backend = CryptographyBackend(module)
elif backend == 'openssl': elif backend == "openssl":
module.debug('Using OpenSSL binary backend') module.debug("Using OpenSSL binary backend")
module_backend = OpenSSLCLIBackend(module) module_backend = OpenSSLCLIBackend(module)
else: else:
module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend)) module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend))
# Check common module parameters # Check common module parameters
if not module.params['validate_certs']: if not module.params["validate_certs"]:
module.warn( module.warn(
'Disabling certificate validation for communications with ACME endpoint. ' "Disabling certificate validation for communications with ACME endpoint. "
'This should only be done for testing against a local ACME server for ' "This should only be done for testing against a local ACME server for "
'development purposes, but *never* for production purposes.' "development purposes, but *never* for production purposes."
) )
if needs_acme_v2 and module.params['acme_version'] < 2: if needs_acme_v2 and module.params["acme_version"] < 2:
module.fail_json(msg='The {0} module requires the ACME v2 protocol!'.format(module._name)) module.fail_json(
msg="The {0} module requires the ACME v2 protocol!".format(module._name)
)
if module.params['acme_version'] == 1: if module.params["acme_version"] == 1:
module.deprecate("The value 1 for 'acme_version' is deprecated. Please switch to ACME v2", module.deprecate(
version='3.0.0', collection_name='community.crypto') "The value 1 for 'acme_version' is deprecated. Please switch to ACME v2",
version="3.0.0",
collection_name="community.crypto",
)
# AnsibleModule() changes the locale, so change it back to C because we rely # AnsibleModule() changes the locale, so change it back to C because we rely
# on datetime.datetime.strptime() when parsing certificate dates. # on datetime.datetime.strptime() when parsing certificate dates.
locale.setlocale(locale.LC_ALL, 'C') locale.setlocale(locale.LC_ALL, "C")
return module_backend return module_backend

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -15,32 +17,21 @@ import os
import traceback import traceback
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.acme.backends import ( from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
CertificateInformation, CertificateInformation,
CryptoBackend, CryptoBackend,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import ( from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import (
ChainMatcher, ChainMatcher,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
BackendException, BackendException,
KeyParsingError, KeyParsingError,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.io import read_file from ansible_collections.community.crypto.plugins.module_utils.acme.io import read_file
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64 nopad_b64,
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
convert_int_to_bytes,
convert_int_to_hex,
) )
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_TIMEZONE, CRYPTOGRAPHY_TIMEZONE,
cryptography_name_to_oid, cryptography_name_to_oid,
@@ -48,45 +39,52 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
get_not_valid_after, get_not_valid_after,
get_not_valid_before, get_not_valid_before,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
convert_int_to_bytes,
convert_int_to_hex,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
extract_first_pem, extract_first_pem,
) )
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,
) )
from ansible_collections.community.crypto.plugins.module_utils.time import ( from ansible_collections.community.crypto.plugins.module_utils.time import (
add_or_remove_timezone, add_or_remove_timezone,
) )
from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
CRYPTOGRAPHY_MINIMAL_VERSION = '1.5'
CRYPTOGRAPHY_MINIMAL_VERSION = "1.5"
CRYPTOGRAPHY_ERROR = None CRYPTOGRAPHY_ERROR = None
try: try:
import cryptography import cryptography
import cryptography.hazmat.backends import cryptography.hazmat.backends
import cryptography.hazmat.primitives.hashes
import cryptography.hazmat.primitives.hmac
import cryptography.hazmat.primitives.asymmetric.ec import cryptography.hazmat.primitives.asymmetric.ec
import cryptography.hazmat.primitives.asymmetric.padding import cryptography.hazmat.primitives.asymmetric.padding
import cryptography.hazmat.primitives.asymmetric.rsa import cryptography.hazmat.primitives.asymmetric.rsa
import cryptography.hazmat.primitives.asymmetric.utils import cryptography.hazmat.primitives.asymmetric.utils
import cryptography.hazmat.primitives.hashes
import cryptography.hazmat.primitives.hmac
import cryptography.hazmat.primitives.serialization import cryptography.hazmat.primitives.serialization
import cryptography.x509 import cryptography.x509
import cryptography.x509.oid import cryptography.x509.oid
except ImportError as dummy: except ImportError:
HAS_CURRENT_CRYPTOGRAPHY = False HAS_CURRENT_CRYPTOGRAPHY = False
CRYPTOGRAPHY_VERSION = None CRYPTOGRAPHY_VERSION = None
CRYPTOGRAPHY_ERROR = traceback.format_exc() CRYPTOGRAPHY_ERROR = traceback.format_exc()
else: else:
CRYPTOGRAPHY_VERSION = cryptography.__version__ CRYPTOGRAPHY_VERSION = cryptography.__version__
HAS_CURRENT_CRYPTOGRAPHY = (LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion(CRYPTOGRAPHY_MINIMAL_VERSION)) HAS_CURRENT_CRYPTOGRAPHY = LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion(
CRYPTOGRAPHY_MINIMAL_VERSION
)
try: try:
if HAS_CURRENT_CRYPTOGRAPHY: if HAS_CURRENT_CRYPTOGRAPHY:
_cryptography_backend = cryptography.hazmat.backends.default_backend() _cryptography_backend = cryptography.hazmat.backends.default_backend()
except Exception as dummy: except Exception:
CRYPTOGRAPHY_ERROR = traceback.format_exc() CRYPTOGRAPHY_ERROR = traceback.format_exc()
@@ -95,13 +93,19 @@ class CryptographyChainMatcher(ChainMatcher):
def _parse_key_identifier(key_identifier, name, criterium_idx, module): def _parse_key_identifier(key_identifier, name, criterium_idx, module):
if key_identifier: if key_identifier:
try: try:
return binascii.unhexlify(key_identifier.replace(':', '')) return binascii.unhexlify(key_identifier.replace(":", ""))
except Exception: except Exception:
if criterium_idx is None: if criterium_idx is None:
module.warn('Criterium has invalid {0} value. Ignoring criterium.'.format(name)) module.warn(
"Criterium has invalid {0} value. Ignoring criterium.".format(
name
)
)
else: else:
module.warn('Criterium {0} in select_chain has invalid {1} value. ' module.warn(
'Ignoring criterium.'.format(criterium_idx, name)) "Criterium {0} in select_chain has invalid {1} value. "
"Ignoring criterium.".format(criterium_idx, name)
)
return None return None
def __init__(self, criterium, module): def __init__(self, criterium, module):
@@ -111,16 +115,26 @@ class CryptographyChainMatcher(ChainMatcher):
self.issuer = [] self.issuer = []
if criterium.subject: if criterium.subject:
self.subject = [ self.subject = [
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.subject, 'subject') (cryptography_name_to_oid(k), to_native(v))
for k, v in parse_name_field(criterium.subject, "subject")
] ]
if criterium.issuer: if criterium.issuer:
self.issuer = [ self.issuer = [
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.issuer, 'issuer') (cryptography_name_to_oid(k), to_native(v))
for k, v in parse_name_field(criterium.issuer, "issuer")
] ]
self.subject_key_identifier = CryptographyChainMatcher._parse_key_identifier( self.subject_key_identifier = CryptographyChainMatcher._parse_key_identifier(
criterium.subject_key_identifier, 'subject_key_identifier', criterium.index, module) criterium.subject_key_identifier,
"subject_key_identifier",
criterium.index,
module,
)
self.authority_key_identifier = CryptographyChainMatcher._parse_key_identifier( self.authority_key_identifier = CryptographyChainMatcher._parse_key_identifier(
criterium.authority_key_identifier, 'authority_key_identifier', criterium.index, module) criterium.authority_key_identifier,
"authority_key_identifier",
criterium.index,
module,
)
def _match_subject(self, x509_subject, match_subject): def _match_subject(self, x509_subject, match_subject):
for oid, value in match_subject: for oid, value in match_subject:
@@ -134,17 +148,19 @@ class CryptographyChainMatcher(ChainMatcher):
return True return True
def match(self, certificate): def match(self, certificate):
''' """
Check whether an alternate chain matches the specified criterium. Check whether an alternate chain matches the specified criterium.
''' """
chain = certificate.chain chain = certificate.chain
if self.test_certificates == 'last': if self.test_certificates == "last":
chain = chain[-1:] chain = chain[-1:]
elif self.test_certificates == 'first': elif self.test_certificates == "first":
chain = chain[:1] chain = chain[:1]
for cert in chain: for cert in chain:
try: try:
x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography.hazmat.backends.default_backend()) x509 = cryptography.x509.load_pem_x509_certificate(
to_bytes(cert), cryptography.hazmat.backends.default_backend()
)
matches = True matches = True
if not self._match_subject(x509.subject, self.subject): if not self._match_subject(x509.subject, self.subject):
matches = False matches = False
@@ -152,14 +168,18 @@ class CryptographyChainMatcher(ChainMatcher):
matches = False matches = False
if self.subject_key_identifier: if self.subject_key_identifier:
try: try:
ext = x509.extensions.get_extension_for_class(cryptography.x509.SubjectKeyIdentifier) ext = x509.extensions.get_extension_for_class(
cryptography.x509.SubjectKeyIdentifier
)
if self.subject_key_identifier != ext.value.digest: if self.subject_key_identifier != ext.value.digest:
matches = False matches = False
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
matches = False matches = False
if self.authority_key_identifier: if self.authority_key_identifier:
try: try:
ext = x509.extensions.get_extension_for_class(cryptography.x509.AuthorityKeyIdentifier) ext = x509.extensions.get_extension_for_class(
cryptography.x509.AuthorityKeyIdentifier
)
if self.authority_key_identifier != ext.value.key_identifier: if self.authority_key_identifier != ext.value.key_identifier:
matches = False matches = False
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
@@ -167,19 +187,23 @@ class CryptographyChainMatcher(ChainMatcher):
if matches: if matches:
return True return True
except Exception as e: except Exception as e:
self.module.warn('Error while loading certificate {0}: {1}'.format(cert, e)) self.module.warn(
"Error while loading certificate {0}: {1}".format(cert, e)
)
return False return False
class CryptographyBackend(CryptoBackend): class CryptographyBackend(CryptoBackend):
def __init__(self, module): def __init__(self, module):
super(CryptographyBackend, self).__init__(module, with_timezone=CRYPTOGRAPHY_TIMEZONE) super(CryptographyBackend, self).__init__(
module, with_timezone=CRYPTOGRAPHY_TIMEZONE
)
def parse_key(self, key_file=None, key_content=None, passphrase=None): def parse_key(self, key_file=None, key_content=None, passphrase=None):
''' """
Parses an RSA or Elliptic Curve key file in PEM format and returns key_data. Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
Raises KeyParsingError in case of errors. Raises KeyParsingError in case of errors.
''' """
# If key_content is not given, read key_file # If key_content is not given, read key_file
if key_content is None: if key_content is None:
key_content = read_file(key_file) key_content = read_file(key_file)
@@ -190,84 +214,97 @@ class CryptographyBackend(CryptoBackend):
key = cryptography.hazmat.primitives.serialization.load_pem_private_key( key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
key_content, key_content,
password=to_bytes(passphrase) if passphrase is not None else None, password=to_bytes(passphrase) if passphrase is not None else None,
backend=_cryptography_backend) backend=_cryptography_backend,
)
except Exception as e: except Exception as e:
raise KeyParsingError('error while loading key: {0}'.format(e)) raise KeyParsingError("error while loading key: {0}".format(e))
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
pk = key.public_key().public_numbers() pk = key.public_key().public_numbers()
return { return {
'key_obj': key, "key_obj": key,
'type': 'rsa', "type": "rsa",
'alg': 'RS256', "alg": "RS256",
'jwk': { "jwk": {
"kty": "RSA", "kty": "RSA",
"e": nopad_b64(convert_int_to_bytes(pk.e)), "e": nopad_b64(convert_int_to_bytes(pk.e)),
"n": nopad_b64(convert_int_to_bytes(pk.n)), "n": nopad_b64(convert_int_to_bytes(pk.n)),
}, },
'hash': 'sha256', "hash": "sha256",
} }
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): elif isinstance(
key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey
):
pk = key.public_key().public_numbers() pk = key.public_key().public_numbers()
if pk.curve.name == 'secp256r1': if pk.curve.name == "secp256r1":
bits = 256 bits = 256
alg = 'ES256' alg = "ES256"
hashalg = 'sha256' hashalg = "sha256"
point_size = 32 point_size = 32
curve = 'P-256' curve = "P-256"
elif pk.curve.name == 'secp384r1': elif pk.curve.name == "secp384r1":
bits = 384 bits = 384
alg = 'ES384' alg = "ES384"
hashalg = 'sha384' hashalg = "sha384"
point_size = 48 point_size = 48
curve = 'P-384' curve = "P-384"
elif pk.curve.name == 'secp521r1': elif pk.curve.name == "secp521r1":
# Not yet supported on Let's Encrypt side, see # Not yet supported on Let's Encrypt side, see
# https://github.com/letsencrypt/boulder/issues/2217 # https://github.com/letsencrypt/boulder/issues/2217
bits = 521 bits = 521
alg = 'ES512' alg = "ES512"
hashalg = 'sha512' hashalg = "sha512"
point_size = 66 point_size = 66
curve = 'P-521' curve = "P-521"
else: else:
raise KeyParsingError('unknown elliptic curve: {0}'.format(pk.curve.name)) raise KeyParsingError(
"unknown elliptic curve: {0}".format(pk.curve.name)
)
num_bytes = (bits + 7) // 8 num_bytes = (bits + 7) // 8
return { return {
'key_obj': key, "key_obj": key,
'type': 'ec', "type": "ec",
'alg': alg, "alg": alg,
'jwk': { "jwk": {
"kty": "EC", "kty": "EC",
"crv": curve, "crv": curve,
"x": nopad_b64(convert_int_to_bytes(pk.x, count=num_bytes)), "x": nopad_b64(convert_int_to_bytes(pk.x, count=num_bytes)),
"y": nopad_b64(convert_int_to_bytes(pk.y, count=num_bytes)), "y": nopad_b64(convert_int_to_bytes(pk.y, count=num_bytes)),
}, },
'hash': hashalg, "hash": hashalg,
'point_size': point_size, "point_size": point_size,
} }
else: else:
raise KeyParsingError('unknown key type "{0}"'.format(type(key))) raise KeyParsingError('unknown key type "{0}"'.format(type(key)))
def sign(self, payload64, protected64, key_data): def sign(self, payload64, protected64, key_data):
sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8') sign_payload = "{0}.{1}".format(protected64, payload64).encode("utf8")
if 'mac_obj' in key_data: if "mac_obj" in key_data:
mac = key_data['mac_obj']() mac = key_data["mac_obj"]()
mac.update(sign_payload) mac.update(sign_payload)
signature = mac.finalize() signature = mac.finalize()
elif isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): elif isinstance(
key_data["key_obj"],
cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey,
):
padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15() padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15()
hashalg = cryptography.hazmat.primitives.hashes.SHA256 hashalg = cryptography.hazmat.primitives.hashes.SHA256
signature = key_data['key_obj'].sign(sign_payload, padding, hashalg()) signature = key_data["key_obj"].sign(sign_payload, padding, hashalg())
elif isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): elif isinstance(
if key_data['hash'] == 'sha256': key_data["key_obj"],
cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey,
):
if key_data["hash"] == "sha256":
hashalg = cryptography.hazmat.primitives.hashes.SHA256 hashalg = cryptography.hazmat.primitives.hashes.SHA256
elif key_data['hash'] == 'sha384': elif key_data["hash"] == "sha384":
hashalg = cryptography.hazmat.primitives.hashes.SHA384 hashalg = cryptography.hazmat.primitives.hashes.SHA384
elif key_data['hash'] == 'sha512': elif key_data["hash"] == "sha512":
hashalg = cryptography.hazmat.primitives.hashes.SHA512 hashalg = cryptography.hazmat.primitives.hashes.SHA512
ecdsa = cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hashalg()) ecdsa = cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hashalg())
r, s = cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature(key_data['key_obj'].sign(sign_payload, ecdsa)) r, s = cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature(
rr = convert_int_to_hex(r, 2 * key_data['point_size']) key_data["key_obj"].sign(sign_payload, ecdsa)
ss = convert_int_to_hex(s, 2 * key_data['point_size']) )
rr = convert_int_to_hex(r, 2 * key_data["point_size"])
ss = convert_int_to_hex(s, 2 * key_data["point_size"])
signature = binascii.unhexlify(rr) + binascii.unhexlify(ss) signature = binascii.unhexlify(rr) + binascii.unhexlify(ss)
return { return {
@@ -277,44 +314,50 @@ class CryptographyBackend(CryptoBackend):
} }
def create_mac_key(self, alg, key): def create_mac_key(self, alg, key):
'''Create a MAC key.''' """Create a MAC key."""
if alg == 'HS256': if alg == "HS256":
hashalg = cryptography.hazmat.primitives.hashes.SHA256 hashalg = cryptography.hazmat.primitives.hashes.SHA256
hashbytes = 32 hashbytes = 32
elif alg == 'HS384': elif alg == "HS384":
hashalg = cryptography.hazmat.primitives.hashes.SHA384 hashalg = cryptography.hazmat.primitives.hashes.SHA384
hashbytes = 48 hashbytes = 48
elif alg == 'HS512': elif alg == "HS512":
hashalg = cryptography.hazmat.primitives.hashes.SHA512 hashalg = cryptography.hazmat.primitives.hashes.SHA512
hashbytes = 64 hashbytes = 64
else: else:
raise BackendException('Unsupported MAC key algorithm for cryptography backend: {0}'.format(alg)) raise BackendException(
"Unsupported MAC key algorithm for cryptography backend: {0}".format(
alg
)
)
key_bytes = base64.urlsafe_b64decode(key) key_bytes = base64.urlsafe_b64decode(key)
if len(key_bytes) < hashbytes: if len(key_bytes) < hashbytes:
raise BackendException( raise BackendException(
'{0} key must be at least {1} bytes long (after Base64 decoding)'.format(alg, hashbytes)) "{0} key must be at least {1} bytes long (after Base64 decoding)".format(
alg, hashbytes
)
)
return { return {
'mac_obj': lambda: cryptography.hazmat.primitives.hmac.HMAC( "mac_obj": lambda: cryptography.hazmat.primitives.hmac.HMAC(
key_bytes, key_bytes, hashalg(), _cryptography_backend
hashalg(), ),
_cryptography_backend), "type": "hmac",
'type': 'hmac', "alg": alg,
'alg': alg, "jwk": {
'jwk': { "kty": "oct",
'kty': 'oct', "k": key,
'k': key,
}, },
} }
def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None): def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None):
''' """
Return a list of requested identifiers (CN and SANs) for the CSR. Return a list of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'. 'dns' or 'ip'.
The list is deduplicated, and if a CNAME is present, it will be returned The list is deduplicated, and if a CNAME is present, it will be returned
as the first element in the result. as the first element in the result.
''' """
if csr_content is None: if csr_content is None:
csr_content = read_file(csr_filename) csr_content = read_file(csr_filename)
else: else:
@@ -332,34 +375,43 @@ class CryptographyBackend(CryptoBackend):
for sub in csr.subject: for sub in csr.subject:
if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME: if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME:
add_identifier(('dns', sub.value)) add_identifier(("dns", sub.value))
for extension in csr.extensions: for extension in csr.extensions:
if extension.oid == cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME: if (
extension.oid
== cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
):
for name in extension.value: for name in extension.value:
if isinstance(name, cryptography.x509.DNSName): if isinstance(name, cryptography.x509.DNSName):
add_identifier(('dns', name.value)) add_identifier(("dns", name.value))
elif isinstance(name, cryptography.x509.IPAddress): elif isinstance(name, cryptography.x509.IPAddress):
add_identifier(('ip', name.value.compressed)) add_identifier(("ip", name.value.compressed))
else: else:
raise BackendException('Found unsupported SAN identifier {0}'.format(name)) raise BackendException(
"Found unsupported SAN identifier {0}".format(name)
)
return result return result
def get_csr_identifiers(self, csr_filename=None, csr_content=None): def get_csr_identifiers(self, csr_filename=None, csr_content=None):
''' """
Return a set of requested identifiers (CN and SANs) for the CSR. Return a set of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'. 'dns' or 'ip'.
''' """
return set(self.get_ordered_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content)) return set(
self.get_ordered_csr_identifiers(
csr_filename=csr_filename, csr_content=csr_content
)
)
def get_cert_days(self, cert_filename=None, cert_content=None, now=None): def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
''' """
Return the days the certificate in cert_filename remains valid and -1 Return the days the certificate in cert_filename remains valid and -1
if the file was not found. If cert_filename contains more than one if the file was not found. If cert_filename contains more than one
certificate, only the first one will be considered. certificate, only the first one will be considered.
If now is not specified, datetime.datetime.now() is used. If now is not specified, datetime.datetime.now() is used.
''' """
if cert_filename is not None: if cert_filename is not None:
cert_content = None cert_content = None
if os.path.exists(cert_filename): if os.path.exists(cert_filename):
@@ -371,14 +423,18 @@ class CryptographyBackend(CryptoBackend):
return -1 return -1
# Make sure we have at most one PEM. Otherwise cryptography 36.0.0 will barf. # 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 '') 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:
if cert_filename is None: if cert_filename is None:
raise BackendException('Cannot parse certificate: {0}'.format(e)) raise BackendException("Cannot parse certificate: {0}".format(e))
raise BackendException('Cannot parse certificate {0}: {1}'.format(cert_filename, e)) raise BackendException(
"Cannot parse certificate {0}: {1}".format(cert_filename, e)
)
if now is None: if now is None:
now = self.get_now() now = self.get_now()
@@ -387,40 +443,48 @@ class CryptographyBackend(CryptoBackend):
return (get_not_valid_after(cert) - now).days return (get_not_valid_after(cert) - now).days
def create_chain_matcher(self, criterium): def create_chain_matcher(self, criterium):
''' """
Given a Criterium object, creates a ChainMatcher object. Given a Criterium object, creates a ChainMatcher object.
''' """
return CryptographyChainMatcher(criterium, self.module) return CryptographyChainMatcher(criterium, self.module)
def get_cert_information(self, cert_filename=None, cert_content=None): def get_cert_information(self, cert_filename=None, cert_content=None):
''' """
Return some information on a X.509 certificate as a CertificateInformation object. Return some information on a X.509 certificate as a CertificateInformation object.
''' """
if cert_filename is not None: if cert_filename is not None:
cert_content = read_file(cert_filename) cert_content = read_file(cert_filename)
else: else:
cert_content = to_bytes(cert_content) cert_content = to_bytes(cert_content)
# Make sure we have at most one PEM. Otherwise cryptography 36.0.0 will barf. # 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 '') 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:
if cert_filename is None: if cert_filename is None:
raise BackendException('Cannot parse certificate: {0}'.format(e)) raise BackendException("Cannot parse certificate: {0}".format(e))
raise BackendException('Cannot parse certificate {0}: {1}'.format(cert_filename, e)) raise BackendException(
"Cannot parse certificate {0}: {1}".format(cert_filename, e)
)
ski = None ski = None
try: try:
ext = cert.extensions.get_extension_for_class(cryptography.x509.SubjectKeyIdentifier) ext = cert.extensions.get_extension_for_class(
cryptography.x509.SubjectKeyIdentifier
)
ski = ext.value.digest ski = ext.value.digest
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
pass pass
aki = None aki = None
try: try:
ext = cert.extensions.get_extension_for_class(cryptography.x509.AuthorityKeyIdentifier) ext = cert.extensions.get_extension_for_class(
cryptography.x509.AuthorityKeyIdentifier
)
aki = ext.value.key_identifier aki = ext.value.key_identifier
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
pass pass

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -17,23 +19,25 @@ import re
import tempfile import tempfile
import traceback import traceback
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes 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 (
CertificateInformation, CertificateInformation,
CryptoBackend, CryptoBackend,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
BackendException, BackendException,
KeyParsingError, KeyParsingError,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
nopad_b64,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
convert_bytes_to_int,
)
from ansible_collections.community.crypto.plugins.module_utils.time import (
ensure_utc_timezone,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import convert_bytes_to_int
from ansible_collections.community.crypto.plugins.module_utils.time import ensure_utc_timezone
try: try:
import ipaddress import ipaddress
@@ -41,7 +45,7 @@ except ImportError:
pass pass
_OPENSSL_ENVIRONMENT_UPDATE = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') _OPENSSL_ENVIRONMENT_UPDATE = dict(LANG="C", LC_ALL="C", LC_MESSAGES="C", LC_CTYPE="C")
def _extract_date(out_text, name, cert_filename_suffix=""): def _extract_date(out_text, name, cert_filename_suffix=""):
@@ -51,11 +55,17 @@ def _extract_date(out_text, name, cert_filename_suffix=""):
# even though the information is there and a supported timezone for all supported # even though the information is there and a supported timezone for all supported
# Python implementations (GMT). So we have to modify the datetime object by # Python implementations (GMT). So we have to modify the datetime object by
# replacing it by UTC. # replacing it by UTC.
return ensure_utc_timezone(datetime.datetime.strptime(date_str, '%b %d %H:%M:%S %Y %Z')) return ensure_utc_timezone(
datetime.datetime.strptime(date_str, "%b %d %H:%M:%S %Y %Z")
)
except AttributeError: except AttributeError:
raise BackendException("No '{0}' date found{1}".format(name, cert_filename_suffix)) raise BackendException(
"No '{0}' date found{1}".format(name, cert_filename_suffix)
)
except ValueError as exc: except ValueError as exc:
raise BackendException("Failed to parse '{0}' date{1}: {2}".format(name, cert_filename_suffix, exc)) raise BackendException(
"Failed to parse '{0}' date{1}: {2}".format(name, cert_filename_suffix, exc)
)
def _decode_octets(octets_text): def _decode_octets(octets_text):
@@ -65,7 +75,11 @@ def _decode_octets(octets_text):
def _extract_octets(out_text, name, required=True, potential_prefixes=None): def _extract_octets(out_text, name, required=True, potential_prefixes=None):
regexp = r"\s+%s:\s*\n\s+%s([A-Fa-f0-9]{2}(?::[A-Fa-f0-9]{2})*)\s*\n" % ( regexp = r"\s+%s:\s*\n\s+%s([A-Fa-f0-9]{2}(?::[A-Fa-f0-9]{2})*)\s*\n" % (
name, name,
('(?:%s)' % '|'.join(re.escape(pp) for pp in potential_prefixes)) if potential_prefixes else '', (
("(?:%s)" % "|".join(re.escape(pp) for pp in potential_prefixes))
if potential_prefixes
else ""
),
) )
match = re.search(regexp, out_text, re.MULTILINE | re.DOTALL) match = re.search(regexp, out_text, re.MULTILINE | re.DOTALL)
if match is not None: if match is not None:
@@ -79,36 +93,41 @@ class OpenSSLCLIBackend(CryptoBackend):
def __init__(self, module, openssl_binary=None): def __init__(self, module, openssl_binary=None):
super(OpenSSLCLIBackend, self).__init__(module, with_timezone=True) super(OpenSSLCLIBackend, self).__init__(module, with_timezone=True)
if openssl_binary is None: if openssl_binary is None:
openssl_binary = module.get_bin_path('openssl', True) openssl_binary = module.get_bin_path("openssl", True)
self.openssl_binary = openssl_binary self.openssl_binary = openssl_binary
def parse_key(self, key_file=None, key_content=None, passphrase=None): def parse_key(self, key_file=None, key_content=None, passphrase=None):
''' """
Parses an RSA or Elliptic Curve key file in PEM format and returns key_data. Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
Raises KeyParsingError in case of errors. Raises KeyParsingError in case of errors.
''' """
if passphrase is not None: if passphrase is not None:
raise KeyParsingError('openssl backend does not support key passphrases') raise KeyParsingError("openssl backend does not support key passphrases")
# If key_file is not given, but key_content, write that to a temporary file # If key_file is not given, but key_content, write that to a temporary file
if key_file is None: if key_file is None:
fd, tmpsrc = tempfile.mkstemp() fd, tmpsrc = tempfile.mkstemp()
self.module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit self.module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
f = os.fdopen(fd, 'wb') f = os.fdopen(fd, "wb")
try: try:
f.write(key_content.encode('utf-8')) f.write(key_content.encode("utf-8"))
key_file = tmpsrc key_file = tmpsrc
except Exception as err: except Exception as err:
try: try:
f.close() f.close()
except Exception as dummy: except Exception:
pass pass
raise KeyParsingError("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc()) raise KeyParsingError(
"failed to create temporary content file: %s" % to_native(err),
exception=traceback.format_exc(),
)
f.close() f.close()
# Parse key # Parse key
account_key_type = None account_key_type = None
with open(key_file, "rt") as f: with open(key_file, "rt") as f:
for line in f: for line in f:
m = re.match(r"^\s*-{5,}BEGIN\s+(EC|RSA)\s+PRIVATE\s+KEY-{5,}\s*$", line) m = re.match(
r"^\s*-{5,}BEGIN\s+(EC|RSA)\s+PRIVATE\s+KEY-{5,}\s*$", line
)
if m is not None: if m is not None:
account_key_type = m.group(1).lower() account_key_type = m.group(1).lower()
break break
@@ -121,111 +140,162 @@ class OpenSSLCLIBackend(CryptoBackend):
if account_key_type not in ("rsa", "ec"): if account_key_type not in ("rsa", "ec"):
raise KeyParsingError('unknown key type "%s"' % account_key_type) raise KeyParsingError('unknown key type "%s"' % account_key_type)
openssl_keydump_cmd = [self.openssl_binary, account_key_type, "-in", key_file, "-noout", "-text"] openssl_keydump_cmd = [
self.openssl_binary,
account_key_type,
"-in",
key_file,
"-noout",
"-text",
]
rc, out, err = self.module.run_command( rc, out, err = self.module.run_command(
openssl_keydump_cmd, check_rc=False, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) openssl_keydump_cmd,
check_rc=False,
environ_update=_OPENSSL_ENVIRONMENT_UPDATE,
)
if rc != 0: if rc != 0:
raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_keydump_cmd), stderr=to_text(err))) raise BackendException(
"Error while running {cmd}: {stderr}".format(
cmd=" ".join(openssl_keydump_cmd), stderr=to_text(err)
)
)
out_text = to_text(out, errors='surrogate_or_strict') out_text = to_text(out, errors="surrogate_or_strict")
if account_key_type == 'rsa': if account_key_type == "rsa":
pub_hex = re.search(r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent", out_text, re.MULTILINE | re.DOTALL).group(1) pub_hex = re.search(
r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent",
out_text,
re.MULTILINE | re.DOTALL,
).group(1)
pub_exp = re.search(r"\npublicExponent: ([0-9]+)", out_text, re.MULTILINE | re.DOTALL).group(1) pub_exp = re.search(
r"\npublicExponent: ([0-9]+)", out_text, re.MULTILINE | re.DOTALL
).group(1)
pub_exp = "{0:x}".format(int(pub_exp)) pub_exp = "{0:x}".format(int(pub_exp))
if len(pub_exp) % 2: if len(pub_exp) % 2:
pub_exp = "0{0}".format(pub_exp) pub_exp = "0{0}".format(pub_exp)
return { return {
'key_file': key_file, "key_file": key_file,
'type': 'rsa', "type": "rsa",
'alg': 'RS256', "alg": "RS256",
'jwk': { "jwk": {
"kty": "RSA", "kty": "RSA",
"e": nopad_b64(binascii.unhexlify(pub_exp.encode("utf-8"))), "e": nopad_b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
"n": nopad_b64(_decode_octets(pub_hex)), "n": nopad_b64(_decode_octets(pub_hex)),
}, },
'hash': 'sha256', "hash": "sha256",
} }
elif account_key_type == 'ec': elif account_key_type == "ec":
pub_data = re.search( pub_data = re.search(
r"pub:\s*\n\s+04:([a-f0-9\:\s]+?)\nASN1 OID: (\S+)(?:\nNIST CURVE: (\S+))?", r"pub:\s*\n\s+04:([a-f0-9\:\s]+?)\nASN1 OID: (\S+)(?:\nNIST CURVE: (\S+))?",
out_text, out_text,
re.MULTILINE | re.DOTALL, re.MULTILINE | re.DOTALL,
) )
if pub_data is None: if pub_data is None:
raise KeyParsingError('cannot parse elliptic curve key') raise KeyParsingError("cannot parse elliptic curve key")
pub_hex = _decode_octets(pub_data.group(1)) pub_hex = _decode_octets(pub_data.group(1))
asn1_oid_curve = pub_data.group(2).lower() asn1_oid_curve = pub_data.group(2).lower()
nist_curve = pub_data.group(3).lower() if pub_data.group(3) else None nist_curve = pub_data.group(3).lower() if pub_data.group(3) else None
if asn1_oid_curve == 'prime256v1' or nist_curve == 'p-256': if asn1_oid_curve == "prime256v1" or nist_curve == "p-256":
bits = 256 bits = 256
alg = 'ES256' alg = "ES256"
hashalg = 'sha256' hashalg = "sha256"
point_size = 32 point_size = 32
curve = 'P-256' curve = "P-256"
elif asn1_oid_curve == 'secp384r1' or nist_curve == 'p-384': elif asn1_oid_curve == "secp384r1" or nist_curve == "p-384":
bits = 384 bits = 384
alg = 'ES384' alg = "ES384"
hashalg = 'sha384' hashalg = "sha384"
point_size = 48 point_size = 48
curve = 'P-384' curve = "P-384"
elif asn1_oid_curve == 'secp521r1' or nist_curve == 'p-521': elif asn1_oid_curve == "secp521r1" or nist_curve == "p-521":
# Not yet supported on Let's Encrypt side, see # Not yet supported on Let's Encrypt side, see
# https://github.com/letsencrypt/boulder/issues/2217 # https://github.com/letsencrypt/boulder/issues/2217
bits = 521 bits = 521
alg = 'ES512' alg = "ES512"
hashalg = 'sha512' hashalg = "sha512"
point_size = 66 point_size = 66
curve = 'P-521' curve = "P-521"
else: else:
raise KeyParsingError('unknown elliptic curve: %s / %s' % (asn1_oid_curve, nist_curve)) raise KeyParsingError(
"unknown elliptic curve: %s / %s" % (asn1_oid_curve, nist_curve)
)
num_bytes = (bits + 7) // 8 num_bytes = (bits + 7) // 8
if len(pub_hex) != 2 * num_bytes: if len(pub_hex) != 2 * num_bytes:
raise KeyParsingError('bad elliptic curve point (%s / %s)' % (asn1_oid_curve, nist_curve)) raise KeyParsingError(
"bad elliptic curve point (%s / %s)" % (asn1_oid_curve, nist_curve)
)
return { return {
'key_file': key_file, "key_file": key_file,
'type': 'ec', "type": "ec",
'alg': alg, "alg": alg,
'jwk': { "jwk": {
"kty": "EC", "kty": "EC",
"crv": curve, "crv": curve,
"x": nopad_b64(pub_hex[:num_bytes]), "x": nopad_b64(pub_hex[:num_bytes]),
"y": nopad_b64(pub_hex[num_bytes:]), "y": nopad_b64(pub_hex[num_bytes:]),
}, },
'hash': hashalg, "hash": hashalg,
'point_size': point_size, "point_size": point_size,
} }
def sign(self, payload64, protected64, key_data): def sign(self, payload64, protected64, key_data):
sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8') sign_payload = "{0}.{1}".format(protected64, payload64).encode("utf8")
if key_data['type'] == 'hmac': if key_data["type"] == "hmac":
hex_key = to_native(binascii.hexlify(base64.urlsafe_b64decode(key_data['jwk']['k']))) hex_key = to_native(
cmd_postfix = ["-mac", "hmac", "-macopt", "hexkey:{0}".format(hex_key), "-binary"] binascii.hexlify(base64.urlsafe_b64decode(key_data["jwk"]["k"]))
)
cmd_postfix = [
"-mac",
"hmac",
"-macopt",
"hexkey:{0}".format(hex_key),
"-binary",
]
else: else:
cmd_postfix = ["-sign", key_data['key_file']] cmd_postfix = ["-sign", key_data["key_file"]]
openssl_sign_cmd = [self.openssl_binary, "dgst", "-{0}".format(key_data['hash'])] + cmd_postfix openssl_sign_cmd = [
self.openssl_binary,
"dgst",
"-{0}".format(key_data["hash"]),
] + cmd_postfix
rc, out, err = self.module.run_command( rc, out, err = self.module.run_command(
openssl_sign_cmd, data=sign_payload, check_rc=False, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) openssl_sign_cmd,
data=sign_payload,
check_rc=False,
binary_data=True,
environ_update=_OPENSSL_ENVIRONMENT_UPDATE,
)
if rc != 0: if rc != 0:
raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_sign_cmd), stderr=to_text(err))) raise BackendException(
"Error while running {cmd}: {stderr}".format(
cmd=" ".join(openssl_sign_cmd), stderr=to_text(err)
)
)
if key_data['type'] == 'ec': if key_data["type"] == "ec":
dummy, der_out, dummy = self.module.run_command( dummy, der_out, dummy = self.module.run_command(
[self.openssl_binary, "asn1parse", "-inform", "DER"], [self.openssl_binary, "asn1parse", "-inform", "DER"],
data=out, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) data=out,
expected_len = 2 * key_data['point_size'] binary_data=True,
environ_update=_OPENSSL_ENVIRONMENT_UPDATE,
)
expected_len = 2 * key_data["point_size"]
sig = re.findall( sig = re.findall(
r"prim:\s+INTEGER\s+:([0-9A-F]{1,%s})\n" % expected_len, r"prim:\s+INTEGER\s+:([0-9A-F]{1,%s})\n" % expected_len,
to_text(der_out, errors='surrogate_or_strict')) to_text(der_out, errors="surrogate_or_strict"),
)
if len(sig) != 2: if len(sig) != 2:
raise BackendException( raise BackendException(
"failed to generate Elliptic Curve signature; cannot parse DER output: {0}".format( "failed to generate Elliptic Curve signature; cannot parse DER output: {0}".format(
to_text(der_out, errors='surrogate_or_strict'))) to_text(der_out, errors="surrogate_or_strict")
sig[0] = (expected_len - len(sig[0])) * '0' + sig[0] )
sig[1] = (expected_len - len(sig[1])) * '0' + sig[1] )
sig[0] = (expected_len - len(sig[0])) * "0" + sig[0]
sig[1] = (expected_len - len(sig[1])) * "0" + sig[1]
out = binascii.unhexlify(sig[0]) + binascii.unhexlify(sig[1]) out = binascii.unhexlify(sig[0]) + binascii.unhexlify(sig[1])
return { return {
@@ -235,30 +305,35 @@ class OpenSSLCLIBackend(CryptoBackend):
} }
def create_mac_key(self, alg, key): def create_mac_key(self, alg, key):
'''Create a MAC key.''' """Create a MAC key."""
if alg == 'HS256': if alg == "HS256":
hashalg = 'sha256' hashalg = "sha256"
hashbytes = 32 hashbytes = 32
elif alg == 'HS384': elif alg == "HS384":
hashalg = 'sha384' hashalg = "sha384"
hashbytes = 48 hashbytes = 48
elif alg == 'HS512': elif alg == "HS512":
hashalg = 'sha512' hashalg = "sha512"
hashbytes = 64 hashbytes = 64
else: else:
raise BackendException('Unsupported MAC key algorithm for OpenSSL backend: {0}'.format(alg)) raise BackendException(
"Unsupported MAC key algorithm for OpenSSL backend: {0}".format(alg)
)
key_bytes = base64.urlsafe_b64decode(key) key_bytes = base64.urlsafe_b64decode(key)
if len(key_bytes) < hashbytes: if len(key_bytes) < hashbytes:
raise BackendException( raise BackendException(
'{0} key must be at least {1} bytes long (after Base64 decoding)'.format(alg, hashbytes)) "{0} key must be at least {1} bytes long (after Base64 decoding)".format(
alg, hashbytes
)
)
return { return {
'type': 'hmac', "type": "hmac",
'alg': alg, "alg": alg,
'jwk': { "jwk": {
'kty': 'oct', "kty": "oct",
'k': key, "k": key,
}, },
'hash': hashalg, "hash": hashalg,
} }
@staticmethod @staticmethod
@@ -270,25 +345,41 @@ class OpenSSLCLIBackend(CryptoBackend):
return ip return ip
def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None): def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None):
''' """
Return a list of requested identifiers (CN and SANs) for the CSR. Return a list of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'. 'dns' or 'ip'.
The list is deduplicated, and if a CNAME is present, it will be returned The list is deduplicated, and if a CNAME is present, it will be returned
as the first element in the result. as the first element in the result.
''' """
filename = csr_filename filename = csr_filename
data = None data = None
if csr_content is not None: if csr_content is not None:
filename = '/dev/stdin' 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",
]
rc, out, err = self.module.run_command( rc, out, err = self.module.run_command(
openssl_csr_cmd, data=data, check_rc=False, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) openssl_csr_cmd,
data=data,
check_rc=False,
binary_data=True,
environ_update=_OPENSSL_ENVIRONMENT_UPDATE,
)
if rc != 0: if rc != 0:
raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_csr_cmd), stderr=to_text(err))) raise BackendException(
"Error while running {cmd}: {stderr}".format(
cmd=" ".join(openssl_csr_cmd), stderr=to_text(err)
)
)
identifiers = set() identifiers = set()
result = [] result = []
@@ -299,61 +390,90 @@ class OpenSSLCLIBackend(CryptoBackend):
identifiers.add(identifier) identifiers.add(identifier)
result.append(identifier) result.append(identifier)
common_name = re.search(r"Subject:.* CN\s?=\s?([^\s,;/]+)", to_text(out, errors='surrogate_or_strict')) common_name = re.search(
r"Subject:.* CN\s?=\s?([^\s,;/]+)",
to_text(out, errors="surrogate_or_strict"),
)
if common_name is not None: if common_name is not None:
add_identifier(('dns', common_name.group(1))) add_identifier(("dns", common_name.group(1)))
subject_alt_names = re.search( subject_alt_names = re.search(
r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n", r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n",
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL) to_text(out, errors="surrogate_or_strict"),
re.MULTILINE | re.DOTALL,
)
if subject_alt_names is not None: if subject_alt_names is not None:
for san in subject_alt_names.group(1).split(", "): for san in subject_alt_names.group(1).split(", "):
if san.lower().startswith("dns:"): if san.lower().startswith("dns:"):
add_identifier(('dns', san[4:])) add_identifier(("dns", san[4:]))
elif san.lower().startswith("ip:"): elif san.lower().startswith("ip:"):
add_identifier(('ip', self._normalize_ip(san[3:]))) add_identifier(("ip", self._normalize_ip(san[3:])))
elif san.lower().startswith("ip address:"): elif san.lower().startswith("ip address:"):
add_identifier(('ip', self._normalize_ip(san[11:]))) add_identifier(("ip", self._normalize_ip(san[11:])))
else: else:
raise BackendException('Found unsupported SAN identifier "{0}"'.format(san)) raise BackendException(
'Found unsupported SAN identifier "{0}"'.format(san)
)
return result return result
def get_csr_identifiers(self, csr_filename=None, csr_content=None): def get_csr_identifiers(self, csr_filename=None, csr_content=None):
''' """
Return a set of requested identifiers (CN and SANs) for the CSR. Return a set of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'. 'dns' or 'ip'.
''' """
return set(self.get_ordered_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content)) return set(
self.get_ordered_csr_identifiers(
csr_filename=csr_filename, csr_content=csr_content
)
)
def get_cert_days(self, cert_filename=None, cert_content=None, now=None): def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
''' """
Return the days the certificate in cert_filename remains valid and -1 Return the days the certificate in cert_filename remains valid and -1
if the file was not found. If cert_filename contains more than one if the file was not found. If cert_filename contains more than one
certificate, only the first one will be considered. certificate, only the first one will be considered.
If now is not specified, datetime.datetime.now() is used. If now is not specified, datetime.datetime.now() is used.
''' """
filename = cert_filename filename = cert_filename
data = None data = None
if cert_content is not None: if cert_content is not None:
filename = '/dev/stdin' 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:
if not os.path.exists(cert_filename): if not os.path.exists(cert_filename):
return -1 return -1
cert_filename_suffix = ' in {0}'.format(cert_filename) cert_filename_suffix = " in {0}".format(cert_filename)
else: else:
return -1 return -1
openssl_cert_cmd = [self.openssl_binary, "x509", "-in", filename, "-noout", "-text"] openssl_cert_cmd = [
self.openssl_binary,
"x509",
"-in",
filename,
"-noout",
"-text",
]
rc, out, err = self.module.run_command( rc, out, err = self.module.run_command(
openssl_cert_cmd, data=data, check_rc=False, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) openssl_cert_cmd,
data=data,
check_rc=False,
binary_data=True,
environ_update=_OPENSSL_ENVIRONMENT_UPDATE,
)
if rc != 0: if rc != 0:
raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_cert_cmd), stderr=to_text(err))) raise BackendException(
"Error while running {cmd}: {stderr}".format(
cmd=" ".join(openssl_cert_cmd), stderr=to_text(err)
)
)
out_text = to_text(out, errors='surrogate_or_strict') out_text = to_text(out, errors="surrogate_or_strict")
not_after = _extract_date(out_text, 'Not After', cert_filename_suffix=cert_filename_suffix) not_after = _extract_date(
out_text, "Not After", cert_filename_suffix=cert_filename_suffix
)
if now is None: if now is None:
now = self.get_now() now = self.get_now()
else: else:
@@ -361,45 +481,76 @@ class OpenSSLCLIBackend(CryptoBackend):
return (not_after - now).days return (not_after - now).days
def create_chain_matcher(self, criterium): def create_chain_matcher(self, criterium):
''' """
Given a Criterium object, creates a ChainMatcher object. Given a Criterium object, creates a ChainMatcher object.
''' """
raise BackendException('Alternate chain matching can only be used with the "cryptography" backend.') raise BackendException(
'Alternate chain matching can only be used with the "cryptography" backend.'
)
def get_cert_information(self, cert_filename=None, cert_content=None): def get_cert_information(self, cert_filename=None, cert_content=None):
''' """
Return some information on a X.509 certificate as a CertificateInformation object. Return some information on a X.509 certificate as a CertificateInformation object.
''' """
filename = cert_filename filename = cert_filename
data = None data = None
if cert_filename is not None: if cert_filename is not None:
cert_filename_suffix = ' in {0}'.format(cert_filename) cert_filename_suffix = " in {0}".format(cert_filename)
else: else:
filename = '/dev/stdin' filename = "/dev/stdin"
data = to_bytes(cert_content) data = to_bytes(cert_content)
cert_filename_suffix = '' cert_filename_suffix = ""
openssl_cert_cmd = [self.openssl_binary, "x509", "-in", filename, "-noout", "-text"] openssl_cert_cmd = [
self.openssl_binary,
"x509",
"-in",
filename,
"-noout",
"-text",
]
rc, out, err = self.module.run_command( rc, out, err = self.module.run_command(
openssl_cert_cmd, data=data, check_rc=False, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE) openssl_cert_cmd,
data=data,
check_rc=False,
binary_data=True,
environ_update=_OPENSSL_ENVIRONMENT_UPDATE,
)
if rc != 0: if rc != 0:
raise BackendException('Error while running {cmd}: {stderr}'.format(cmd=' '.join(openssl_cert_cmd), stderr=to_text(err))) raise BackendException(
"Error while running {cmd}: {stderr}".format(
cmd=" ".join(openssl_cert_cmd), stderr=to_text(err)
)
)
out_text = to_text(out, errors='surrogate_or_strict') out_text = to_text(out, errors="surrogate_or_strict")
not_after = _extract_date(out_text, 'Not After', cert_filename_suffix=cert_filename_suffix) not_after = _extract_date(
not_before = _extract_date(out_text, 'Not Before', cert_filename_suffix=cert_filename_suffix) out_text, "Not After", cert_filename_suffix=cert_filename_suffix
)
not_before = _extract_date(
out_text, "Not Before", cert_filename_suffix=cert_filename_suffix
)
sn = re.search( sn = re.search(
r" Serial Number: ([0-9]+)", r" Serial Number: ([0-9]+)",
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL) to_text(out, errors="surrogate_or_strict"),
re.MULTILINE | re.DOTALL,
)
if sn: if sn:
serial = int(sn.group(1)) serial = int(sn.group(1))
else: else:
serial = convert_bytes_to_int(_extract_octets(out_text, 'Serial Number', required=True)) serial = convert_bytes_to_int(
_extract_octets(out_text, "Serial Number", required=True)
)
ski = _extract_octets(out_text, 'X509v3 Subject Key Identifier', required=False) ski = _extract_octets(out_text, "X509v3 Subject Key Identifier", required=False)
aki = _extract_octets(out_text, 'X509v3 Authority Key Identifier', required=False, potential_prefixes=['keyid:', '']) aki = _extract_octets(
out_text,
"X509v3 Authority Key Identifier",
required=False,
potential_prefixes=["keyid:", ""],
)
return CertificateInformation( return CertificateInformation(
not_valid_after=not_after, not_valid_after=not_after,

View File

@@ -6,49 +6,50 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from collections import namedtuple
import abc import abc
import datetime import datetime
import re import re
from collections import namedtuple
from ansible.module_utils import six from ansible.module_utils import six
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
BackendException, BackendException,
) )
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.time import ( from ansible_collections.community.crypto.plugins.module_utils.time import (
UTC,
ensure_utc_timezone, ensure_utc_timezone,
from_epoch_seconds, from_epoch_seconds,
get_epoch_seconds, get_epoch_seconds,
get_now_datetime, get_now_datetime,
get_relative_time_option, get_relative_time_option,
remove_timezone, remove_timezone,
UTC,
) )
CertificateInformation = namedtuple( CertificateInformation = namedtuple(
'CertificateInformation', "CertificateInformation",
( (
'not_valid_after', "not_valid_after",
'not_valid_before', "not_valid_before",
'serial_number', "serial_number",
'subject_key_identifier', "subject_key_identifier",
'authority_key_identifier', "authority_key_identifier",
), ),
) )
_FRACTIONAL_MATCHER = re.compile(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(|\.\d+)(Z|[+-]\d{2}:?\d{2}.*)$') _FRACTIONAL_MATCHER = re.compile(
r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(|\.\d+)(Z|[+-]\d{2}:?\d{2}.*)$"
)
def _reduce_fractional_digits(timestamp_str): def _reduce_fractional_digits(timestamp_str):
@@ -58,13 +59,15 @@ def _reduce_fractional_digits(timestamp_str):
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339) # RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
m = _FRACTIONAL_MATCHER.match(timestamp_str) m = _FRACTIONAL_MATCHER.match(timestamp_str)
if not m: if not m:
raise BackendException('Cannot parse ISO 8601 timestamp {0!r}'.format(timestamp_str)) raise BackendException(
"Cannot parse ISO 8601 timestamp {0!r}".format(timestamp_str)
)
timestamp, fractional, timezone = m.groups() timestamp, fractional, timezone = m.groups()
if len(fractional) > 7: if len(fractional) > 7:
# Python does not support anything smaller than microseconds # Python does not support anything smaller than microseconds
# (Golang supports nanoseconds, Boulder often emits more fractional digits, which Python chokes on) # (Golang supports nanoseconds, Boulder often emits more fractional digits, which Python chokes on)
fractional = fractional[:7] fractional = fractional[:7]
return '%s%s%s' % (timestamp, fractional, timezone) return "%s%s%s" % (timestamp, fractional, timezone)
def _parse_acme_timestamp(timestamp_str, with_timezone): def _parse_acme_timestamp(timestamp_str, with_timezone):
@@ -73,15 +76,26 @@ def _parse_acme_timestamp(timestamp_str, with_timezone):
""" """
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339) # RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
timestamp_str = _reduce_fractional_digits(timestamp_str) timestamp_str = _reduce_fractional_digits(timestamp_str)
for format in ('%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S.%fZ', '%Y-%m-%dT%H:%M:%S%z', '%Y-%m-%dT%H:%M:%S.%f%z'): for format in (
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S.%fZ",
"%Y-%m-%dT%H:%M:%S%z",
"%Y-%m-%dT%H:%M:%S.%f%z",
):
# Note that %z will not work with Python 2... https://stackoverflow.com/a/27829491 # Note that %z will not work with Python 2... https://stackoverflow.com/a/27829491
try: try:
result = datetime.datetime.strptime(timestamp_str, format) result = datetime.datetime.strptime(timestamp_str, format)
except ValueError: except ValueError:
pass pass
else: else:
return ensure_utc_timezone(result) if with_timezone else remove_timezone(result) return (
raise BackendException('Cannot parse ISO 8601 timestamp {0!r}'.format(timestamp_str)) ensure_utc_timezone(result)
if with_timezone
else remove_timezone(result)
)
raise BackendException(
"Cannot parse ISO 8601 timestamp {0!r}".format(timestamp_str)
)
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
@@ -99,30 +113,34 @@ class CryptoBackend(object):
def parse_module_parameter(self, value, name): def parse_module_parameter(self, value, name):
try: try:
return get_relative_time_option(value, name, backend='cryptography', with_timezone=self._with_timezone) return get_relative_time_option(
value, name, backend="cryptography", with_timezone=self._with_timezone
)
except OpenSSLObjectError as exc: except OpenSSLObjectError as exc:
raise BackendException(to_native(exc)) raise BackendException(to_native(exc))
def interpolate_timestamp(self, timestamp_start, timestamp_end, percentage): def interpolate_timestamp(self, timestamp_start, timestamp_end, percentage):
start = get_epoch_seconds(timestamp_start) start = get_epoch_seconds(timestamp_start)
end = get_epoch_seconds(timestamp_end) end = get_epoch_seconds(timestamp_end)
return from_epoch_seconds(start + percentage * (end - start), with_timezone=self._with_timezone) return from_epoch_seconds(
start + percentage * (end - start), with_timezone=self._with_timezone
)
def get_utc_datetime(self, *args, **kwargs): def get_utc_datetime(self, *args, **kwargs):
kwargs_ext = dict(kwargs) kwargs_ext = dict(kwargs)
if self._with_timezone and ('tzinfo' not in kwargs_ext and len(args) < 8): if self._with_timezone and ("tzinfo" not in kwargs_ext and len(args) < 8):
kwargs_ext['tzinfo'] = UTC kwargs_ext["tzinfo"] = UTC
result = datetime.datetime(*args, **kwargs_ext) result = datetime.datetime(*args, **kwargs_ext)
if self._with_timezone and ('tzinfo' in kwargs or len(args) >= 8): if self._with_timezone and ("tzinfo" in kwargs or len(args) >= 8):
result = ensure_utc_timezone(result) result = ensure_utc_timezone(result)
return result return result
@abc.abstractmethod @abc.abstractmethod
def parse_key(self, key_file=None, key_content=None, passphrase=None): def parse_key(self, key_file=None, key_content=None, passphrase=None):
''' """
Parses an RSA or Elliptic Curve key file in PEM format and returns key_data. Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
Raises KeyParsingError in case of errors. Raises KeyParsingError in case of errors.
''' """
@abc.abstractmethod @abc.abstractmethod
def sign(self, payload64, protected64, key_data): def sign(self, payload64, protected64, key_data):
@@ -130,54 +148,56 @@ class CryptoBackend(object):
@abc.abstractmethod @abc.abstractmethod
def create_mac_key(self, alg, key): def create_mac_key(self, alg, key):
'''Create a MAC key.''' """Create a MAC key."""
def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None): def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None):
''' """
Return a list of requested identifiers (CN and SANs) for the CSR. Return a list of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'. 'dns' or 'ip'.
The list is deduplicated, and if a CNAME is present, it will be returned The list is deduplicated, and if a CNAME is present, it will be returned
as the first element in the result. as the first element in the result.
''' """
self.module.deprecate( self.module.deprecate(
"Every backend must override the get_ordered_csr_identifiers() method." "Every backend must override the get_ordered_csr_identifiers() method."
" The default implementation will be removed in 3.0.0 and this method will be marked as `abstractmethod` by then.", " The default implementation will be removed in 3.0.0 and this method will be marked as `abstractmethod` by then.",
version='3.0.0', version="3.0.0",
collection_name='community.crypto', collection_name="community.crypto",
)
return sorted(
self.get_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content)
) )
return sorted(self.get_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content))
@abc.abstractmethod @abc.abstractmethod
def get_csr_identifiers(self, csr_filename=None, csr_content=None): def get_csr_identifiers(self, csr_filename=None, csr_content=None):
''' """
Return a set of requested identifiers (CN and SANs) for the CSR. Return a set of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'. 'dns' or 'ip'.
''' """
@abc.abstractmethod @abc.abstractmethod
def get_cert_days(self, cert_filename=None, cert_content=None, now=None): def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
''' """
Return the days the certificate in cert_filename remains valid and -1 Return the days the certificate in cert_filename remains valid and -1
if the file was not found. If cert_filename contains more than one if the file was not found. If cert_filename contains more than one
certificate, only the first one will be considered. certificate, only the first one will be considered.
If now is not specified, datetime.datetime.now() is used. If now is not specified, datetime.datetime.now() is used.
''' """
@abc.abstractmethod @abc.abstractmethod
def create_chain_matcher(self, criterium): def create_chain_matcher(self, criterium):
''' """
Given a Criterium object, creates a ChainMatcher object. Given a Criterium object, creates a ChainMatcher object.
''' """
def get_cert_information(self, cert_filename=None, cert_content=None): def get_cert_information(self, cert_filename=None, cert_content=None):
''' """
Return some information on a X.509 certificate as a CertificateInformation object. Return some information on a X.509 certificate as a CertificateInformation object.
''' """
# Not implementing this method in a backend is DEPRECATED and will be # Not implementing this method in a backend is DEPRECATED and will be
# disallowed in community.crypto 3.0.0. This method will be marked as # disallowed in community.crypto 3.0.0. This method will be marked as
# @abstractmethod by then. # @abstractmethod by then.
raise BackendException('This backend does not support get_cert_information()') raise BackendException("This backend does not support get_cert_information()")

View File

@@ -5,79 +5,76 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import os import os
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
ACMEClient,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.account import ( from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
ACMEAccount, ACMEAccount,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import ( ACMEClient,
Authorization,
wait_for_validation,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import ( from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import (
CertificateChain, CertificateChain,
Criterium, Criterium,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import (
Authorization,
wait_for_validation,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ModuleFailException, ModuleFailException,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.io import write_file
from ansible_collections.community.crypto.plugins.module_utils.acme.orders import ( from ansible_collections.community.crypto.plugins.module_utils.acme.orders import Order
Order,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.io import (
write_file,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
pem_to_der, pem_to_der,
) )
class ACMECertificateClient(object): class ACMECertificateClient(object):
''' """
ACME v2 client class. Uses an ACME account object and a CSR to ACME v2 client class. Uses an ACME account object and a CSR to
start and validate ACME challenges and download the respective start and validate ACME challenges and download the respective
certificates. certificates.
''' """
def __init__(self, module, backend, client=None, account=None): def __init__(self, module, backend, client=None, account=None):
self.module = module self.module = module
self.version = module.params['acme_version'] self.version = module.params["acme_version"]
self.csr = module.params.get('csr') self.csr = module.params.get("csr")
self.csr_content = module.params.get('csr_content') self.csr_content = module.params.get("csr_content")
if client is None: if client is None:
client = ACMEClient(module, backend) client = ACMEClient(module, backend)
self.client = client self.client = client
if account is None: if account is None:
account = ACMEAccount(self.client) account = ACMEAccount(self.client)
self.account = account self.account = account
self.order_uri = module.params.get('order_uri') self.order_uri = module.params.get("order_uri")
self.order_creation_error_strategy = module.params.get('order_creation_error_strategy', 'auto') self.order_creation_error_strategy = module.params.get(
self.order_creation_max_retries = module.params.get('order_creation_max_retries', 3) "order_creation_error_strategy", "auto"
)
self.order_creation_max_retries = module.params.get(
"order_creation_max_retries", 3
)
# Make sure account exists # Make sure account exists
dummy, account_data = self.account.setup_account(allow_creation=False) dummy, account_data = self.account.setup_account(allow_creation=False)
if account_data is None: if account_data is None:
raise ModuleFailException(msg='Account does not exist or is deactivated.') raise ModuleFailException(msg="Account does not exist or is deactivated.")
if self.csr is not None and not os.path.exists(self.csr): if self.csr is not None and not os.path.exists(self.csr):
raise ModuleFailException("CSR %s not found" % (self.csr)) raise ModuleFailException("CSR %s not found" % (self.csr))
# Extract list of identifiers from CSR # Extract list of identifiers from CSR
if self.csr is not None or self.csr_content is not None: if self.csr is not None or self.csr_content is not None:
self.identifiers = self.client.backend.get_ordered_csr_identifiers(csr_filename=self.csr, csr_content=self.csr_content) self.identifiers = self.client.backend.get_ordered_csr_identifiers(
csr_filename=self.csr, csr_content=self.csr_content
)
else: else:
self.identifiers = None self.identifiers = None
@@ -87,24 +84,31 @@ class ACMECertificateClient(object):
for criterium_idx, criterium in enumerate(select_chain): for criterium_idx, criterium in enumerate(select_chain):
try: try:
select_chain_matcher.append( select_chain_matcher.append(
self.client.backend.create_chain_matcher(Criterium(criterium, index=criterium_idx))) self.client.backend.create_chain_matcher(
Criterium(criterium, index=criterium_idx)
)
)
except ValueError as exc: except ValueError as exc:
self.module.warn('Error while parsing criterium: {error}. Ignoring criterium.'.format(error=exc)) self.module.warn(
"Error while parsing criterium: {error}. Ignoring criterium.".format(
error=exc
)
)
return select_chain_matcher return select_chain_matcher
def load_order(self): def load_order(self):
if not self.order_uri: if not self.order_uri:
raise ModuleFailException('The order URI has not been provided') raise ModuleFailException("The order URI has not been provided")
order = Order.from_url(self.client, self.order_uri) order = Order.from_url(self.client, self.order_uri)
order.load_authorizations(self.client) order.load_authorizations(self.client)
return order return order
def create_order(self, replaces_cert_id=None, profile=None): def create_order(self, replaces_cert_id=None, profile=None):
''' """
Create a new order. Create a new order.
''' """
if self.identifiers is None: if self.identifiers is None:
raise ModuleFailException('No identifiers have been provided') raise ModuleFailException("No identifiers have been provided")
order = Order.create_with_error_handling( order = Order.create_with_error_handling(
self.client, self.client,
self.identifiers, self.identifiers,
@@ -119,64 +123,78 @@ class ACMECertificateClient(object):
return order return order
def get_challenges_data(self, order): def get_challenges_data(self, order):
''' """
Get challenge details. Get challenge details.
Return a tuple of generic challenge details, and specialized DNS challenge details. Return a tuple of generic challenge details, and specialized DNS challenge details.
''' """
# Get general challenge data # Get general challenge data
data = [] data = []
for authz in order.authorizations.values(): for authz in order.authorizations.values():
# Skip valid authentications: their challenges are already valid # Skip valid authentications: their challenges are already valid
# and do not need to be returned # and do not need to be returned
if authz.status == 'valid': if authz.status == "valid":
continue continue
data.append(dict( data.append(
dict(
identifier=authz.identifier, identifier=authz.identifier,
identifier_type=authz.identifier_type, identifier_type=authz.identifier_type,
challenges=authz.get_challenge_data(self.client), challenges=authz.get_challenge_data(self.client),
)) )
)
# Get DNS challenge data # Get DNS challenge data
data_dns = {} data_dns = {}
dns_challenge_type = 'dns-01' dns_challenge_type = "dns-01"
for entry in data: for entry in data:
dns_challenge = entry['challenges'].get(dns_challenge_type) dns_challenge = entry["challenges"].get(dns_challenge_type)
if dns_challenge: if dns_challenge:
values = data_dns.get(dns_challenge['record']) values = data_dns.get(dns_challenge["record"])
if values is None: if values is None:
values = [] values = []
data_dns[dns_challenge['record']] = values data_dns[dns_challenge["record"]] = values
values.append(dns_challenge['resource_value']) values.append(dns_challenge["resource_value"])
return data, data_dns return data, data_dns
def check_that_authorizations_can_be_used(self, order): def check_that_authorizations_can_be_used(self, order):
bad_authzs = [] bad_authzs = []
for authz in order.authorizations.values(): for authz in order.authorizations.values():
if authz.status not in ('valid', 'pending'): if authz.status not in ("valid", "pending"):
bad_authzs.append('{authz} (status={status!r})'.format( bad_authzs.append(
"{authz} (status={status!r})".format(
authz=authz.combined_identifier, authz=authz.combined_identifier,
status=authz.status, status=authz.status,
)) )
)
if bad_authzs: if bad_authzs:
raise ModuleFailException( raise ModuleFailException(
'Some of the authorizations for the order are in a bad state, so the order' "Some of the authorizations for the order are in a bad state, so the order"
' can no longer be satisfied: {bad_authzs}'.format( " can no longer be satisfied: {bad_authzs}".format(
bad_authzs=', '.join(sorted(bad_authzs)), bad_authzs=", ".join(sorted(bad_authzs)),
), ),
) )
def collect_invalid_authzs(self, order): def collect_invalid_authzs(self, order):
return [authz for authz in order.authorizations.values() if authz.status == 'invalid'] return [
authz
for authz in order.authorizations.values()
if authz.status == "invalid"
]
def collect_pending_authzs(self, order): def collect_pending_authzs(self, order):
return [authz for authz in order.authorizations.values() if authz.status == 'pending'] return [
authz
for authz in order.authorizations.values()
if authz.status == "pending"
]
def call_validate(self, pending_authzs, get_challenge, wait=True): def call_validate(self, pending_authzs, get_challenge, wait=True):
authzs_with_challenges_to_wait_for = [] authzs_with_challenges_to_wait_for = []
for authz in pending_authzs: for authz in pending_authzs:
challenge_type = get_challenge(authz) challenge_type = get_challenge(authz)
authz.call_validate(self.client, challenge_type, wait=wait) authz.call_validate(self.client, challenge_type, wait=wait)
authzs_with_challenges_to_wait_for.append((authz, challenge_type, authz.find_challenge(challenge_type))) authzs_with_challenges_to_wait_for.append(
(authz, challenge_type, authz.find_challenge(challenge_type))
)
return authzs_with_challenges_to_wait_for return authzs_with_challenges_to_wait_for
def wait_for_validation(self, authzs_to_wait_for): def wait_for_validation(self, authzs_to_wait_for):
@@ -188,27 +206,45 @@ class ACMECertificateClient(object):
try: try:
alt_cert = CertificateChain.download(self.client, alternate) alt_cert = CertificateChain.download(self.client, alternate)
except ModuleFailException as e: except ModuleFailException as e:
self.module.warn('Error while downloading alternative certificate {0}: {1}'.format(alternate, e)) self.module.warn(
"Error while downloading alternative certificate {0}: {1}".format(
alternate, e
)
)
continue continue
if alt_cert.cert is not None: if alt_cert.cert is not None:
alternate_chains.append(alt_cert) alternate_chains.append(alt_cert)
else: else:
self.module.warn('Error while downloading alternative certificate {0}: no certificate found'.format(alternate)) self.module.warn(
"Error while downloading alternative certificate {0}: no certificate found".format(
alternate
)
)
return alternate_chains return alternate_chains
def download_certificate(self, order, download_all_chains=True): def download_certificate(self, order, download_all_chains=True):
''' """
Download certificate from a valid oder. Download certificate from a valid oder.
''' """
if order.status != 'valid': if order.status != "valid":
raise ModuleFailException('The order must be valid, but has state {state!r}!'.format(state=order.state)) raise ModuleFailException(
"The order must be valid, but has state {state!r}!".format(
state=order.state
)
)
if not order.certificate_uri: if not order.certificate_uri:
raise ModuleFailException("Order's crtificate URL {url!r} is empty!".format(url=order.certificate_uri)) raise ModuleFailException(
"Order's crtificate URL {url!r} is empty!".format(
url=order.certificate_uri
)
)
cert = CertificateChain.download(self.client, order.certificate_uri) cert = CertificateChain.download(self.client, order.certificate_uri)
if cert.cert is None: if cert.cert is None:
raise ModuleFailException('Certificate at {url} is empty!'.format(url=order.certificate_uri)) raise ModuleFailException(
"Certificate at {url} is empty!".format(url=order.certificate_uri)
)
alternate_chains = None alternate_chains = None
if download_all_chains: if download_all_chains:
@@ -217,15 +253,18 @@ class ACMECertificateClient(object):
return cert, alternate_chains return cert, alternate_chains
def get_certificate(self, order, download_all_chains=True): def get_certificate(self, order, download_all_chains=True):
''' """
Request a new certificate and downloads it, and optionally all certificate chains. Request a new certificate and downloads it, and optionally all certificate chains.
First verifies whether all authorizations are valid; if not, aborts with an error. First verifies whether all authorizations are valid; if not, aborts with an error.
''' """
if self.csr is None and self.csr_content is None: if self.csr is None and self.csr_content is None:
raise ModuleFailException('No CSR has been provided') raise ModuleFailException("No CSR has been provided")
for identifier, authz in order.authorizations.items(): for identifier, authz in order.authorizations.items():
if authz.status != 'valid': if authz.status != "valid":
authz.raise_error('Status is {status!r} and not "valid"'.format(status=authz.status), module=self.module) authz.raise_error(
'Status is {status!r} and not "valid"'.format(status=authz.status),
module=self.module,
)
order.finalize(self.client, pem_to_der(self.csr, self.csr_content)) order.finalize(self.client, pem_to_der(self.csr, self.csr_content))
@@ -235,30 +274,40 @@ class ACMECertificateClient(object):
for criterium_idx, matcher in enumerate(select_chain_matcher): for criterium_idx, matcher in enumerate(select_chain_matcher):
for chain in chains: for chain in chains:
if matcher.match(chain): if matcher.match(chain):
self.module.debug('Found matching chain for criterium {0}'.format(criterium_idx)) self.module.debug(
"Found matching chain for criterium {0}".format(criterium_idx)
)
return chain return chain
return None return None
def write_cert_chain(self, cert, cert_dest=None, fullchain_dest=None, chain_dest=None): def write_cert_chain(
self, cert, cert_dest=None, fullchain_dest=None, chain_dest=None
):
changed = False changed = False
if cert_dest and write_file(self.module, cert_dest, cert.cert.encode('utf8')): if cert_dest and write_file(self.module, cert_dest, cert.cert.encode("utf8")):
changed = True changed = True
if fullchain_dest and write_file(self.module, fullchain_dest, (cert.cert + "\n".join(cert.chain)).encode('utf8')): if fullchain_dest and write_file(
self.module,
fullchain_dest,
(cert.cert + "\n".join(cert.chain)).encode("utf8"),
):
changed = True changed = True
if chain_dest and write_file(self.module, chain_dest, ("\n".join(cert.chain)).encode('utf8')): if chain_dest and write_file(
self.module, chain_dest, ("\n".join(cert.chain)).encode("utf8")
):
changed = True changed = True
return changed return changed
def deactivate_authzs(self, order): def deactivate_authzs(self, order):
''' """
Deactivates all valid authz's. Does not raise exceptions. Deactivates all valid authz's. Does not raise exceptions.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2 https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2 https://tools.ietf.org/html/rfc8555#section-7.5.2
''' """
if len(order.authorization_uris) > len(order.authorizations): if len(order.authorization_uris) > len(order.authorizations):
for authz_uri in order.authorization_uris: for authz_uri in order.authorization_uris:
authz = None authz = None
@@ -267,8 +316,12 @@ class ACMECertificateClient(object):
except Exception: except Exception:
# ignore errors # ignore errors
pass pass
if authz is None or authz.status != 'deactivated': if authz is None or authz.status != "deactivated":
self.module.warn(warning='Could not deactivate authz object {0}.'.format(authz_uri)) self.module.warn(
warning="Could not deactivate authz object {0}.".format(
authz_uri
)
)
else: else:
for authz in order.authorizations.values(): for authz in order.authorizations.values():
try: try:
@@ -276,5 +329,9 @@ class ACMECertificateClient(object):
except Exception: except Exception:
# ignore errors # ignore errors
pass pass
if authz.status != 'deactivated': if authz.status != "deactivated":
self.module.warn(warning='Could not deactivate authz object {0}.'.format(authz.url)) self.module.warn(
warning="Could not deactivate authz object {0}.".format(
authz.url
)
)

View File

@@ -6,33 +6,32 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import abc import abc
from ansible.module_utils import six from ansible.module_utils import six
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ModuleFailException, ModuleFailException,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
der_to_pem, der_to_pem,
nopad_b64, nopad_b64,
process_links, process_links,
) )
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,
) )
class CertificateChain(object): class CertificateChain(object):
''' """
Download and parse the certificate chain. Download and parse the certificate chain.
https://tools.ietf.org/html/rfc8555#section-7.4.2 https://tools.ietf.org/html/rfc8555#section-7.4.2
''' """
def __init__(self, url): def __init__(self, url):
self.url = url self.url = url
@@ -42,86 +41,106 @@ class CertificateChain(object):
@classmethod @classmethod
def download(cls, client, url): def download(cls, client, url):
content, info = client.get_request(url, parse_json_result=False, headers={'Accept': 'application/pem-certificate-chain'}) content, info = client.get_request(
url,
parse_json_result=False,
headers={"Accept": "application/pem-certificate-chain"},
)
if not content or not info['content-type'].startswith('application/pem-certificate-chain'): if not content or not info["content-type"].startswith(
"application/pem-certificate-chain"
):
raise ModuleFailException( raise ModuleFailException(
"Cannot download certificate chain from {0}, as content type is not application/pem-certificate-chain: {1} (headers: {2})".format( "Cannot download certificate chain from {0}, as content type is not application/pem-certificate-chain: {1} (headers: {2})".format(
url, content, info)) url, content, info
)
)
result = cls(url) result = cls(url)
# Parse data # Parse data
certs = split_pem_list(content.decode('utf-8'), keep_inbetween=True) certs = split_pem_list(content.decode("utf-8"), keep_inbetween=True)
if certs: if certs:
result.cert = certs[0] result.cert = certs[0]
result.chain = certs[1:] result.chain = certs[1:]
process_links(info, lambda link, relation: result._process_links(client, link, relation)) process_links(
info, lambda link, relation: result._process_links(client, link, relation)
)
if result.cert is None: if result.cert is None:
raise ModuleFailException("Failed to parse certificate chain download from {0}: {1} (headers: {2})".format(url, content, info)) raise ModuleFailException(
"Failed to parse certificate chain download from {0}: {1} (headers: {2})".format(
url, content, info
)
)
return result return result
def _process_links(self, client, link, relation): def _process_links(self, client, link, relation):
if relation == 'up': if relation == "up":
# Process link-up headers if there was no chain in reply # Process link-up headers if there was no chain in reply
if not self.chain: if not self.chain:
chain_result, chain_info = client.get_request(link, parse_json_result=False) chain_result, chain_info = client.get_request(
if chain_info['status'] in [200, 201]: link, parse_json_result=False
)
if chain_info["status"] in [200, 201]:
self.chain.append(der_to_pem(chain_result)) self.chain.append(der_to_pem(chain_result))
elif relation == 'alternate': elif relation == "alternate":
self.alternates.append(link) self.alternates.append(link)
def to_json(self): def to_json(self):
cert = self.cert.encode('utf8') cert = self.cert.encode("utf8")
chain = ('\n'.join(self.chain)).encode('utf8') chain = ("\n".join(self.chain)).encode("utf8")
return { return {
'cert': cert, "cert": cert,
'chain': chain, "chain": chain,
'full_chain': cert + chain, "full_chain": cert + chain,
} }
class Criterium(object): class Criterium(object):
def __init__(self, criterium, index=None): def __init__(self, criterium, index=None):
self.index = index self.index = index
self.test_certificates = criterium['test_certificates'] self.test_certificates = criterium["test_certificates"]
self.subject = criterium['subject'] self.subject = criterium["subject"]
self.issuer = criterium['issuer'] self.issuer = criterium["issuer"]
self.subject_key_identifier = criterium['subject_key_identifier'] self.subject_key_identifier = criterium["subject_key_identifier"]
self.authority_key_identifier = criterium['authority_key_identifier'] self.authority_key_identifier = criterium["authority_key_identifier"]
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class ChainMatcher(object): class ChainMatcher(object):
@abc.abstractmethod @abc.abstractmethod
def match(self, certificate): def match(self, certificate):
''' """
Check whether a certificate chain (CertificateChain instance) matches. Check whether a certificate chain (CertificateChain instance) matches.
''' """
def retrieve_acme_v1_certificate(client, csr_der): def retrieve_acme_v1_certificate(client, csr_der):
''' """
Create a new certificate based on the CSR (ACME v1 protocol). Create a new certificate based on the CSR (ACME v1 protocol).
Return the certificate object as dict Return the certificate object as dict
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5 https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
''' """
new_cert = { new_cert = {
"resource": "new-cert", "resource": "new-cert",
"csr": nopad_b64(csr_der), "csr": nopad_b64(csr_der),
} }
result, info = client.send_signed_request( result, info = client.send_signed_request(
client.directory['new-cert'], new_cert, error_msg='Failed to receive certificate', expected_status_codes=[200, 201]) client.directory["new-cert"],
cert = CertificateChain(info['location']) new_cert,
error_msg="Failed to receive certificate",
expected_status_codes=[200, 201],
)
cert = CertificateChain(info["location"])
cert.cert = der_to_pem(result) cert.cert = der_to_pem(result)
def f(link, relation): def f(link, relation):
if relation == 'up': if relation == "up":
chain_result, chain_info = client.get_request(link, parse_json_result=False) chain_result, chain_info = client.get_request(link, parse_json_result=False)
if chain_info['status'] in [200, 201]: if chain_info["status"] in [200, 201]:
del cert.chain[:] del cert.chain[:]
cert.chain.append(der_to_pem(chain_result)) cert.chain.append(der_to_pem(chain_result))

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -16,16 +18,15 @@ import re
import time import time
from ansible.module_utils.common.text.converters import to_bytes from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ACMEProtocolException,
ModuleFailException,
format_error_problem,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
nopad_b64, nopad_b64,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
format_error_problem,
ACMEProtocolException,
ModuleFailException,
)
try: try:
import ipaddress import ipaddress
@@ -34,17 +35,19 @@ except ImportError:
def create_key_authorization(client, token): def create_key_authorization(client, token):
''' """
Returns the key authorization for the given token Returns the key authorization for the given token
https://tools.ietf.org/html/rfc8555#section-8.1 https://tools.ietf.org/html/rfc8555#section-8.1
''' """
accountkey_json = json.dumps(client.account_jwk, sort_keys=True, separators=(',', ':')) accountkey_json = json.dumps(
thumbprint = nopad_b64(hashlib.sha256(accountkey_json.encode('utf8')).digest()) client.account_jwk, sort_keys=True, separators=(",", ":")
)
thumbprint = nopad_b64(hashlib.sha256(accountkey_json.encode("utf8")).digest())
return "{0}.{1}".format(token, thumbprint) return "{0}.{1}".format(token, thumbprint)
def combine_identifier(identifier_type, identifier): def combine_identifier(identifier_type, identifier):
return '{type}:{identifier}'.format(type=identifier_type, identifier=identifier) return "{type}:{identifier}".format(type=identifier_type, identifier=identifier)
def normalize_combined_identifier(identifier): def normalize_combined_identifier(identifier):
@@ -55,10 +58,13 @@ def normalize_combined_identifier(identifier):
def split_identifier(identifier): def split_identifier(identifier):
parts = identifier.split(':', 1) parts = identifier.split(":", 1)
if len(parts) != 2: if len(parts) != 2:
raise ModuleFailException( raise ModuleFailException(
'Identifier "{identifier}" is not of the form <type>:<identifier>'.format(identifier=identifier)) 'Identifier "{identifier}" is not of the form <type>:<identifier>'.format(
identifier=identifier
)
)
return parts return parts
@@ -66,27 +72,27 @@ class Challenge(object):
def __init__(self, data, url): def __init__(self, data, url):
self.data = data self.data = data
self.type = data['type'] self.type = data["type"]
self.url = url self.url = url
self.status = data['status'] self.status = data["status"]
self.token = data.get('token') self.token = data.get("token")
@classmethod @classmethod
def from_json(cls, client, data, url=None): def from_json(cls, client, data, url=None):
return cls(data, url or (data['uri'] if client.version == 1 else data['url'])) return cls(data, url or (data["uri"] if client.version == 1 else data["url"]))
def call_validate(self, client): def call_validate(self, client):
challenge_response = {} challenge_response = {}
if client.version == 1: if client.version == 1:
token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token) token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token)
key_authorization = create_key_authorization(client, token) key_authorization = create_key_authorization(client, token)
challenge_response['resource'] = 'challenge' challenge_response["resource"] = "challenge"
challenge_response['keyAuthorization'] = key_authorization challenge_response["keyAuthorization"] = key_authorization
challenge_response['type'] = self.type challenge_response["type"] = self.type
client.send_signed_request( client.send_signed_request(
self.url, self.url,
challenge_response, challenge_response,
error_msg='Failed to validate challenge', error_msg="Failed to validate challenge",
expected_status_codes=[200, 202], expected_status_codes=[200, 202],
) )
@@ -97,40 +103,44 @@ class Challenge(object):
token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token) token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token)
key_authorization = create_key_authorization(client, token) key_authorization = create_key_authorization(client, token)
if self.type == 'http-01': if self.type == "http-01":
# https://tools.ietf.org/html/rfc8555#section-8.3 # https://tools.ietf.org/html/rfc8555#section-8.3
return { return {
'resource': '.well-known/acme-challenge/{token}'.format(token=token), "resource": ".well-known/acme-challenge/{token}".format(token=token),
'resource_value': key_authorization, "resource_value": key_authorization,
} }
if self.type == 'dns-01': if self.type == "dns-01":
if identifier_type != 'dns': if identifier_type != "dns":
return None return None
# https://tools.ietf.org/html/rfc8555#section-8.4 # https://tools.ietf.org/html/rfc8555#section-8.4
resource = '_acme-challenge' resource = "_acme-challenge"
value = nopad_b64(hashlib.sha256(to_bytes(key_authorization)).digest()) value = nopad_b64(hashlib.sha256(to_bytes(key_authorization)).digest())
record = '{0}.{1}'.format(resource, identifier[2:] if identifier.startswith('*.') else identifier) record = "{0}.{1}".format(
resource, identifier[2:] if identifier.startswith("*.") else identifier
)
return { return {
'resource': resource, "resource": resource,
'resource_value': value, "resource_value": value,
'record': record, "record": record,
} }
if self.type == 'tls-alpn-01': if self.type == "tls-alpn-01":
# https://www.rfc-editor.org/rfc/rfc8737.html#section-3 # https://www.rfc-editor.org/rfc/rfc8737.html#section-3
if identifier_type == 'ip': if identifier_type == "ip":
# IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596) # IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596)
resource = ipaddress.ip_address(identifier).reverse_pointer resource = ipaddress.ip_address(identifier).reverse_pointer
if not resource.endswith('.'): if not resource.endswith("."):
resource += '.' resource += "."
else: else:
resource = identifier resource = identifier
value = base64.b64encode(hashlib.sha256(to_bytes(key_authorization)).digest()) value = base64.b64encode(
hashlib.sha256(to_bytes(key_authorization)).digest()
)
return { return {
'resource': resource, "resource": resource,
'resource_original': combine_identifier(identifier_type, identifier), "resource_original": combine_identifier(identifier_type, identifier),
'resource_value': value, "resource_value": value,
} }
# Unknown challenge type: ignore # Unknown challenge type: ignore
@@ -139,25 +149,28 @@ class Challenge(object):
class Authorization(object): class Authorization(object):
def _setup(self, client, data): def _setup(self, client, data):
data['uri'] = self.url data["uri"] = self.url
self.data = data self.data = data
# While 'challenges' is a required field, apparently not every CA cares # While 'challenges' is a required field, apparently not every CA cares
# (https://github.com/ansible-collections/community.crypto/issues/824) # (https://github.com/ansible-collections/community.crypto/issues/824)
if data.get('challenges'): if data.get("challenges"):
self.challenges = [Challenge.from_json(client, challenge) for challenge in data['challenges']] self.challenges = [
Challenge.from_json(client, challenge)
for challenge in data["challenges"]
]
else: else:
self.challenges = [] self.challenges = []
if client.version == 1 and 'status' not in data: if client.version == 1 and "status" not in data:
# https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2 # https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2
# "status (required, string): ... # "status (required, string): ...
# If this field is missing, then the default value is "pending"." # If this field is missing, then the default value is "pending"."
self.status = 'pending' self.status = "pending"
else: else:
self.status = data['status'] self.status = data["status"]
self.identifier = data['identifier']['value'] self.identifier = data["identifier"]["value"]
self.identifier_type = data['identifier']['type'] self.identifier_type = data["identifier"]["type"]
if data.get('wildcard', False): if data.get("wildcard", False):
self.identifier = '*.{0}'.format(self.identifier) self.identifier = "*.{0}".format(self.identifier)
def __init__(self, url): def __init__(self, url):
self.url = url self.url = url
@@ -182,11 +195,11 @@ class Authorization(object):
@classmethod @classmethod
def create(cls, client, identifier_type, identifier): def create(cls, client, identifier_type, identifier):
''' """
Create a new authorization for the given identifier. Create a new authorization for the given identifier.
Return the authorization object of the new authorization Return the authorization object of the new authorization
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4 https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4
''' """
new_authz = { new_authz = {
"identifier": { "identifier": {
"type": identifier_type, "type": identifier_type,
@@ -194,16 +207,22 @@ class Authorization(object):
}, },
} }
if client.version == 1: if client.version == 1:
url = client.directory['new-authz'] url = client.directory["new-authz"]
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(client.module, 'ACME endpoint does not support pre-authorization') raise ACMEProtocolException(
url = client.directory['newAuthz'] client.module, "ACME endpoint does not support pre-authorization"
)
url = client.directory["newAuthz"]
result, info = client.send_signed_request( result, info = client.send_signed_request(
url, new_authz, error_msg='Failed to request challenges', expected_status_codes=[200, 201]) url,
return cls.from_json(client, result, info['location']) new_authz,
error_msg="Failed to request challenges",
expected_status_codes=[200, 201],
)
return cls.from_json(client, result, info["location"])
@property @property
def combined_identifier(self): def combined_identifier(self):
@@ -219,39 +238,44 @@ class Authorization(object):
return changed return changed
def get_challenge_data(self, client): def get_challenge_data(self, client):
''' """
Returns a dict with the data for all proposed (and supported) challenges Returns a dict with the data for all proposed (and supported) challenges
of the given authorization. of the given authorization.
''' """
data = {} data = {}
for challenge in self.challenges: for challenge in self.challenges:
validation_data = challenge.get_validation_data(client, self.identifier_type, self.identifier) validation_data = challenge.get_validation_data(
client, self.identifier_type, self.identifier
)
if validation_data is not None: if validation_data is not None:
data[challenge.type] = validation_data data[challenge.type] = validation_data
return data return data
def raise_error(self, error_msg, module=None): def raise_error(self, error_msg, module=None):
''' """
Aborts with a specific error for a challenge. Aborts with a specific error for a challenge.
''' """
error_details = [] error_details = []
# multiple challenges could have failed at this point, gather error # multiple challenges could have failed at this point, gather error
# details for all of them before failing # details for all of them before failing
for challenge in self.challenges: for challenge in self.challenges:
if challenge.status == 'invalid': if challenge.status == "invalid":
msg = 'Challenge {type}'.format(type=challenge.type) msg = "Challenge {type}".format(type=challenge.type)
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(challenge.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, 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),
), ),
extras=dict( extras=dict(
identifier=self.combined_identifier, identifier=self.combined_identifier,
@@ -268,88 +292,90 @@ class Authorization(object):
def wait_for_validation(self, client, callenge_type): def wait_for_validation(self, client, callenge_type):
while True: while True:
self.refresh(client) self.refresh(client)
if self.status in ['valid', 'invalid', 'revoked']: if self.status in ["valid", "invalid", "revoked"]:
break break
time.sleep(2) time.sleep(2)
if self.status == 'invalid': if self.status == "invalid":
self.raise_error('Status is "invalid"', module=client.module) self.raise_error('Status is "invalid"', module=client.module)
return self.status == 'valid' return self.status == "valid"
def call_validate(self, client, challenge_type, wait=True): def call_validate(self, client, challenge_type, wait=True):
''' """
Validate the authorization provided in the auth dict. Returns True Validate the authorization provided in the auth dict. Returns True
when the validation was successful and False when it was not. when the validation was successful and False when it was not.
''' """
challenge = self.find_challenge(challenge_type) challenge = self.find_challenge(challenge_type)
if challenge is None: if challenge is None:
raise ModuleFailException('Found no challenge of type "{challenge}" for identifier {identifier}!'.format( raise ModuleFailException(
'Found no challenge of type "{challenge}" for identifier {identifier}!'.format(
challenge=challenge_type, challenge=challenge_type,
identifier=self.combined_identifier, identifier=self.combined_identifier,
)) )
)
challenge.call_validate(client) challenge.call_validate(client)
if not wait: if not wait:
return self.status == 'valid' return self.status == "valid"
return self.wait_for_validation(client, challenge_type) return self.wait_for_validation(client, challenge_type)
def can_deactivate(self): def can_deactivate(self):
''' """
Deactivates this authorization. Deactivates this authorization.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2 https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2 https://tools.ietf.org/html/rfc8555#section-7.5.2
''' """
return self.status in ('valid', 'pending') return self.status in ("valid", "pending")
def deactivate(self, client): def deactivate(self, client):
''' """
Deactivates this authorization. Deactivates this authorization.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2 https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2 https://tools.ietf.org/html/rfc8555#section-7.5.2
''' """
if not self.can_deactivate(): if not self.can_deactivate():
return return
authz_deactivate = { authz_deactivate = {"status": "deactivated"}
'status': 'deactivated'
}
if client.version == 1: if client.version == 1:
authz_deactivate['resource'] = 'authz' authz_deactivate["resource"] = "authz"
result, info = client.send_signed_request(self.url, authz_deactivate, fail_on_error=False) result, info = client.send_signed_request(
if 200 <= info['status'] < 300 and result.get('status') == 'deactivated': self.url, authz_deactivate, fail_on_error=False
self.status = 'deactivated' )
if 200 <= info["status"] < 300 and result.get("status") == "deactivated":
self.status = "deactivated"
return True return True
return False return False
@classmethod @classmethod
def deactivate_url(cls, client, url): def deactivate_url(cls, client, url):
''' """
Deactivates this authorization. Deactivates this authorization.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2 https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2 https://tools.ietf.org/html/rfc8555#section-7.5.2
''' """
authz = cls(url) authz = cls(url)
authz_deactivate = { authz_deactivate = {"status": "deactivated"}
'status': 'deactivated'
}
if client.version == 1: if client.version == 1:
authz_deactivate['resource'] = 'authz' authz_deactivate["resource"] = "authz"
result, info = client.send_signed_request(url, authz_deactivate, fail_on_error=True) result, info = client.send_signed_request(
url, authz_deactivate, fail_on_error=True
)
authz._setup(client, result) authz._setup(client, result)
return authz return authz
def wait_for_validation(authzs, client): def wait_for_validation(authzs, client):
''' """
Wait until a list of authz is valid. Fail if at least one of them is invalid or revoked. Wait until a list of authz is valid. Fail if at least one of them is invalid or revoked.
''' """
while authzs: while authzs:
authzs_next = [] authzs_next = []
for authz in authzs: for authz in authzs:
authz.refresh(client) authz.refresh(client)
if authz.status in ['valid', 'invalid', 'revoked']: if authz.status in ["valid", "invalid", "revoked"]:
if authz.status != 'valid': if authz.status != "valid":
authz.raise_error('Status is not "valid"', module=client.module) authz.raise_error('Status is not "valid"', module=client.module)
else: else:
authzs_next.append(authz) authzs_next.append(authz)

View File

@@ -6,10 +6,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
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.common.text.converters import to_text
from ansible.module_utils.six import binary_type, PY3 from ansible.module_utils.six import PY3, binary_type
from ansible.module_utils.six.moves.http_client import responses as http_responses from ansible.module_utils.six.moves.http_client import responses as http_responses
@@ -17,37 +19,42 @@ def format_http_status(status_code):
expl = http_responses.get(status_code) expl = http_responses.get(status_code)
if not expl: if not expl:
return str(status_code) return str(status_code)
return '%d %s' % (status_code, expl) return "%d %s" % (status_code, expl)
def format_error_problem(problem, subproblem_prefix=''): def format_error_problem(problem, subproblem_prefix=""):
error_type = problem.get('type', 'about:blank') # https://www.rfc-editor.org/rfc/rfc7807#section-3.1 error_type = problem.get(
if 'title' in problem: "type", "about:blank"
) # https://www.rfc-editor.org/rfc/rfc7807#section-3.1
if "title" in problem:
msg = 'Error "{title}" ({type})'.format( msg = 'Error "{title}" ({type})'.format(
type=error_type, type=error_type,
title=problem['title'], title=problem["title"],
) )
else: else:
msg = 'Error {type}'.format(type=error_type) msg = "Error {type}".format(type=error_type)
if 'detail' in problem: if "detail" in problem:
msg += ': "{detail}"'.format(detail=problem['detail']) msg += ': "{detail}"'.format(detail=problem["detail"])
subproblems = problem.get('subproblems') subproblems = problem.get("subproblems")
if subproblems is not None: if subproblems is not None:
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)
),
) )
return msg return msg
class ModuleFailException(Exception): class ModuleFailException(Exception):
''' """
If raised, module.fail_json() will be called with the given parameters after cleanup. If raised, module.fail_json() will be called with the given parameters after cleanup.
''' """
def __init__(self, msg, **args): def __init__(self, msg, **args):
super(ModuleFailException, self).__init__(self, msg) super(ModuleFailException, self).__init__(self, msg)
self.msg = msg self.msg = msg
@@ -58,7 +65,16 @@ class ModuleFailException(Exception):
class ACMEProtocolException(ModuleFailException): class ACMEProtocolException(ModuleFailException):
def __init__(self, module, msg=None, info=None, response=None, content=None, content_json=None, extras=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:
@@ -68,7 +84,7 @@ class ACMEProtocolException(ModuleFailException):
raise TypeError raise TypeError
content = response.read() content = response.read()
except (AttributeError, TypeError): except (AttributeError, TypeError):
content = info.pop('body', None) content = info.pop("body", None)
# Make sure that content_json is None or a dictionary # Make sure that content_json is None or a dictionary
if content_json is not None and not isinstance(content_json, dict): if content_json is not None and not isinstance(content_json, dict):
@@ -80,7 +96,7 @@ class ACMEProtocolException(ModuleFailException):
if content_json is None and content is not None and module 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(to_text(content)) content_json = module.from_json(to_text(content))
except Exception as e: except Exception:
pass pass
extras = extras or dict() extras = extras or dict()
@@ -88,53 +104,71 @@ class ACMEProtocolException(ModuleFailException):
error_type = None error_type = None
if msg is None: if msg is None:
msg = 'ACME request failed' msg = "ACME request failed"
add_msg = '' add_msg = ""
if info is not None: if info is not None:
url = info['url'] url = info["url"]
code = info['status'] code = info["status"]
extras['http_url'] = url extras["http_url"] = url
extras['http_status'] = code extras["http_status"] = code
error_code = code error_code = code
if code is not None and code >= 400 and content_json is not None and 'type' in content_json: if (
error_type = content_json['type'] code is not None
if 'status' in content_json and content_json['status'] != code: and code >= 400
code_msg = 'status {problem_code} (HTTP status: {http_code})'.format( and content_json is not None
http_code=format_http_status(code), problem_code=content_json['status']) and "type" in content_json
else: ):
code_msg = 'status {problem_code}'.format(problem_code=format_http_status(code)) error_type = content_json["type"]
if code == -1 and info.get('msg'): if "status" in content_json and content_json["status"] != code:
code_msg = 'error: {msg}'.format(msg=info['msg']) code_msg = (
subproblems = content_json.pop('subproblems', None) "status {problem_code} (HTTP status: {http_code})".format(
add_msg = ' {problem}.'.format(problem=format_error_problem(content_json)) http_code=format_http_status(code),
extras['problem'] = content_json problem_code=content_json["status"],
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_msg = 'HTTP status {code}'.format(code=format_http_status(code)) code_msg = "status {problem_code}".format(
if code == -1 and info.get('msg'): problem_code=format_http_status(code)
code_msg = 'error: {msg}'.format(msg=info['msg']) )
if code == -1 and info.get("msg"):
code_msg = "error: {msg}".format(msg=info["msg"])
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:
code_msg = "HTTP status {code}".format(code=format_http_status(code))
if code == -1 and info.get("msg"):
code_msg = "error: {msg}".format(msg=info["msg"])
if content_json is not None: if content_json is not None:
add_msg = ' The JSON error result: {content}'.format(content=content_json) add_msg = " The JSON error result: {content}".format(
content=content_json
)
elif content is not None: elif content is not None:
add_msg = ' The raw error result: {content}'.format(content=to_text(content)) add_msg = " The raw error result: {content}".format(
msg = '{msg} for {url} with {code}'.format(msg=msg, url=url, code=code_msg) content=to_text(content)
)
msg = "{msg} for {url} with {code}".format(msg=msg, url=url, code=code_msg)
elif content_json is not None: elif content_json is not None:
add_msg = ' The JSON result: {content}'.format(content=content_json) add_msg = " The JSON result: {content}".format(content=content_json)
elif content is not None: elif content is not None:
add_msg = ' The raw result: {content}'.format(content=to_text(content)) add_msg = " The raw result: {content}".format(content=to_text(content))
super(ACMEProtocolException, self).__init__( super(ACMEProtocolException, self).__init__(
'{msg}.{add_msg}'.format(msg=msg, add_msg=add_msg), "{msg}.{add_msg}".format(msg=msg, add_msg=add_msg), **extras
**extras
) )
self.problem = {} self.problem = {}
self.subproblems = [] self.subproblems = []

View File

@@ -7,6 +7,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -16,13 +18,14 @@ import tempfile
import traceback import traceback
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException ModuleFailException,
)
def read_file(fn, mode='b'): def read_file(fn, mode="b"):
try: try:
with open(fn, 'r' + mode) as f: with open(fn, "r" + mode) as f:
return f.read() return f.read()
except Exception as e: except Exception as e:
raise ModuleFailException('Error while reading file "{0}": {1}'.format(fn, e)) raise ModuleFailException('Error while reading file "{0}": {1}'.format(fn, e))
@@ -30,23 +33,26 @@ def read_file(fn, mode='b'):
# This function was adapted from an earlier version of https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/uri.py # This function was adapted from an earlier version of https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/uri.py
def write_file(module, dest, content): def write_file(module, dest, content):
''' """
Write content to destination file dest, only if the content Write content to destination file dest, only if the content
has changed. has changed.
''' """
changed = False changed = False
# create a tempfile # create a tempfile
fd, tmpsrc = tempfile.mkstemp(text=False) fd, tmpsrc = tempfile.mkstemp(text=False)
f = os.fdopen(fd, 'wb') f = os.fdopen(fd, "wb")
try: try:
f.write(content) f.write(content)
except Exception as err: except Exception as err:
try: try:
f.close() f.close()
except Exception as dummy: except Exception:
pass pass
os.remove(tmpsrc) os.remove(tmpsrc)
raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc()) raise ModuleFailException(
"failed to create temporary content file: %s" % to_native(err),
exception=traceback.format_exc(),
)
f.close() f.close()
checksum_src = None checksum_src = None
checksum_dest = None checksum_dest = None
@@ -54,7 +60,7 @@ def write_file(module, dest, content):
if not os.path.exists(tmpsrc): if not os.path.exists(tmpsrc):
try: try:
os.remove(tmpsrc) os.remove(tmpsrc)
except Exception as dummy: except Exception:
pass pass
raise ModuleFailException("Source %s does not exist" % (tmpsrc)) raise ModuleFailException("Source %s does not exist" % (tmpsrc))
if not os.access(tmpsrc, os.R_OK): if not os.access(tmpsrc, os.R_OK):
@@ -72,7 +78,7 @@ def write_file(module, dest, content):
raise ModuleFailException("Destination %s not readable" % (dest)) raise ModuleFailException("Destination %s not readable" % (dest))
checksum_dest = module.sha1(dest) checksum_dest = module.sha1(dest)
else: else:
dirname = os.path.dirname(dest) or '.' dirname = os.path.dirname(dest) or "."
if not os.access(dirname, os.W_OK): if not os.access(dirname, os.W_OK):
os.remove(tmpsrc) os.remove(tmpsrc)
raise ModuleFailException("Destination dir %s not writable" % (dirname)) raise ModuleFailException("Destination dir %s not writable" % (dirname))
@@ -82,6 +88,9 @@ def write_file(module, dest, content):
changed = True changed = True
except Exception as err: except Exception as err:
os.remove(tmpsrc) os.remove(tmpsrc)
raise ModuleFailException("failed to copy %s to %s: %s" % (tmpsrc, dest, to_native(err)), exception=traceback.format_exc()) raise ModuleFailException(
"failed to copy %s to %s: %s" % (tmpsrc, dest, to_native(err)),
exception=traceback.format_exc(),
)
os.remove(tmpsrc) os.remove(tmpsrc)
return changed return changed

View File

@@ -6,37 +6,37 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import time import time
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
nopad_b64,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ACMEProtocolException,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import ( from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import (
Authorization, Authorization,
normalize_combined_identifier, normalize_combined_identifier,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ACMEProtocolException,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
nopad_b64,
)
class Order(object): class Order(object):
def _setup(self, client, data): def _setup(self, client, data):
self.data = data self.data = data
self.status = data['status'] self.status = data["status"]
self.identifiers = [] self.identifiers = []
for identifier in data['identifiers']: for identifier in data["identifiers"]:
self.identifiers.append((identifier['type'], identifier['value'])) self.identifiers.append((identifier["type"], identifier["value"]))
self.replaces_cert_id = data.get('replaces') self.replaces_cert_id = data.get("replaces")
self.finalize_uri = data.get('finalize') self.finalize_uri = data.get("finalize")
self.certificate_uri = data.get('certificate') self.certificate_uri = data.get("certificate")
self.authorization_uris = data['authorizations'] self.authorization_uris = data["authorizations"]
self.authorizations = {} self.authorizations = {}
def __init__(self, url): def __init__(self, url):
@@ -66,33 +66,37 @@ class Order(object):
@classmethod @classmethod
def create(cls, client, identifiers, replaces_cert_id=None, profile=None): def create(cls, client, identifiers, replaces_cert_id=None, profile=None):
''' """
Start a new certificate order (ACME v2 protocol). Start a new certificate order (ACME v2 protocol).
https://tools.ietf.org/html/rfc8555#section-7.4 https://tools.ietf.org/html/rfc8555#section-7.4
''' """
acme_identifiers = [] acme_identifiers = []
for identifier_type, identifier in identifiers: for identifier_type, identifier in identifiers:
acme_identifiers.append({ acme_identifiers.append(
'type': identifier_type, {
'value': identifier, "type": identifier_type,
}) "value": identifier,
new_order = {
"identifiers": acme_identifiers
} }
)
new_order = {"identifiers": acme_identifiers}
if replaces_cert_id is not None: if replaces_cert_id is not None:
new_order["replaces"] = replaces_cert_id new_order["replaces"] = replaces_cert_id
if profile is not None: if profile is not None:
new_order["profile"] = profile new_order["profile"] = profile
result, info = client.send_signed_request( result, info = client.send_signed_request(
client.directory['newOrder'], new_order, error_msg='Failed to start new order', expected_status_codes=[201]) client.directory["newOrder"],
return cls.from_json(client, result, info['location']) new_order,
error_msg="Failed to start new order",
expected_status_codes=[201],
)
return cls.from_json(client, result, info["location"])
@classmethod @classmethod
def create_with_error_handling( def create_with_error_handling(
cls, cls,
client, client,
identifiers, identifiers,
error_strategy='auto', error_strategy="auto",
error_max_retries=3, error_max_retries=3,
replaces_cert_id=None, replaces_cert_id=None,
profile=None, profile=None,
@@ -113,20 +117,29 @@ class Order(object):
while True: while True:
tries += 1 tries += 1
try: try:
return cls.create(client, identifiers, replaces_cert_id=replaces_cert_id, profile=profile) return cls.create(
client,
identifiers,
replaces_cert_id=replaces_cert_id,
profile=profile,
)
except ACMEProtocolException as exc: except ACMEProtocolException as exc:
if tries <= error_max_retries + 1 and error_strategy != 'fail': if tries <= error_max_retries + 1 and error_strategy != "fail":
if error_strategy == 'always': if error_strategy == "always":
continue continue
if ( if (
error_strategy in ('auto', 'retry_without_replaces_cert_id') and error_strategy in ("auto", "retry_without_replaces_cert_id")
replaces_cert_id is not None and and replaces_cert_id is not None
not (exc.error_code == 409 and exc.error_type == 'urn:ietf:params:acme:error:alreadyReplaced') and not (
exc.error_code == 409
and exc.error_type
== "urn:ietf:params:acme:error:alreadyReplaced"
)
): ):
if message_callback: if message_callback:
message_callback( message_callback(
'Stop passing `replaces={replaces}` due to error {code} {type} when creating ACME order'.format( "Stop passing `replaces={replaces}` due to error {code} {type} when creating ACME order".format(
code=exc.error_code, code=exc.error_code,
type=exc.error_type, type=exc.error_type,
replaces=replaces_cert_id, replaces=replaces_cert_id,
@@ -146,32 +159,41 @@ class Order(object):
def load_authorizations(self, client): def load_authorizations(self, client):
for auth_uri in self.authorization_uris: for auth_uri in self.authorization_uris:
authz = Authorization.from_url(client, auth_uri) authz = Authorization.from_url(client, auth_uri)
self.authorizations[normalize_combined_identifier(authz.combined_identifier)] = authz self.authorizations[
normalize_combined_identifier(authz.combined_identifier)
] = authz
def wait_for_finalization(self, client): def wait_for_finalization(self, client):
while True: while True:
self.refresh(client) self.refresh(client)
if self.status in ['valid', 'invalid', 'pending', 'ready']: if self.status in ["valid", "invalid", "pending", "ready"]:
break break
time.sleep(2) time.sleep(2)
if self.status != 'valid': if self.status != "valid":
raise ACMEProtocolException( raise ACMEProtocolException(
client.module, client.module,
'Failed to wait for order to complete; got status "{status}"'.format(status=self.status), 'Failed to wait for order to complete; got status "{status}"'.format(
content_json=self.data) status=self.status
),
content_json=self.data,
)
def finalize(self, client, csr_der, wait=True): def finalize(self, client, csr_der, wait=True):
''' """
Create a new certificate based on the csr. Create a new certificate based on the csr.
Return the certificate object as dict Return the certificate object as dict
https://tools.ietf.org/html/rfc8555#section-7.4 https://tools.ietf.org/html/rfc8555#section-7.4
''' """
new_cert = { new_cert = {
"csr": nopad_b64(csr_der), "csr": nopad_b64(csr_der),
} }
result, info = client.send_signed_request( result, info = client.send_signed_request(
self.finalize_uri, new_cert, error_msg='Failed to finalizing order', expected_status_codes=[200]) self.finalize_uri,
new_cert,
error_msg="Failed to finalizing order",
expected_status_codes=[200],
)
# It is not clear from the RFC whether the finalize call returns the order object or not. # It is not clear from the RFC whether the finalize call returns the order object or not.
# Instead of using the result, we call self.refresh(client) below. # Instead of using the result, we call self.refresh(client) below.
@@ -179,9 +201,12 @@ class Order(object):
self.wait_for_finalization(client) self.wait_for_finalization(client)
else: else:
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(
client.module, client.module,
'Failed to finalize order; got status "{status}"'.format(status=self.status), 'Failed to finalize order; got status "{status}"'.format(
status=self.status
),
info=info, info=info,
content_json=result) content_json=result,
)

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -17,32 +19,36 @@ import traceback
from ansible.module_utils.common.text.converters 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 (
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException ModuleFailException,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import convert_int_to_bytes from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
convert_int_to_bytes,
from ansible_collections.community.crypto.plugins.module_utils.time import get_now_datetime )
from ansible_collections.community.crypto.plugins.module_utils.time import (
get_now_datetime,
)
def nopad_b64(data): def nopad_b64(data):
return base64.urlsafe_b64encode(data).decode('utf8').replace("=", "") return base64.urlsafe_b64encode(data).decode("utf8").replace("=", "")
def der_to_pem(der_cert): def der_to_pem(der_cert):
''' """
Convert the DER format certificate in der_cert to a PEM format certificate and return it. Convert the DER format certificate in der_cert to a PEM format certificate and return it.
''' """
return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format( return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format(
"\n".join(textwrap.wrap(base64.b64encode(der_cert).decode('utf8'), 64))) "\n".join(textwrap.wrap(base64.b64encode(der_cert).decode("utf8"), 64))
)
def pem_to_der(pem_filename=None, pem_content=None): def pem_to_der(pem_filename=None, pem_content=None):
''' """
Load PEM file, or use PEM file's content, and convert to DER. Load PEM file, or use PEM file's content, and convert to DER.
If PEM contains multiple entities, the first entity will be used. If PEM contains multiple entities, the first entity will be used.
''' """
certificate_lines = [] certificate_lines = []
if pem_content is not None: if pem_content is not None:
lines = pem_content.splitlines() lines = pem_content.splitlines()
@@ -51,12 +57,17 @@ def pem_to_der(pem_filename=None, pem_content=None):
with open(pem_filename, "rt") as f: with open(pem_filename, "rt") as f:
lines = list(f) lines = list(f)
except Exception as err: except Exception as err:
raise ModuleFailException("cannot load PEM file {0}: {1}".format(pem_filename, to_native(err)), exception=traceback.format_exc()) raise ModuleFailException(
"cannot load PEM file {0}: {1}".format(pem_filename, to_native(err)),
exception=traceback.format_exc(),
)
else: else:
raise ModuleFailException('One of pem_filename and pem_content must be provided') raise ModuleFailException(
"One of pem_filename and pem_content must be provided"
)
header_line_count = 0 header_line_count = 0
for line in lines: for line in lines:
if line.startswith('-----'): if line.startswith("-----"):
header_line_count += 1 header_line_count += 1
if header_line_count == 2: if header_line_count == 2:
# If certificate file contains other certs appended # If certificate file contains other certs appended
@@ -64,27 +75,27 @@ def pem_to_der(pem_filename=None, pem_content=None):
break break
continue continue
certificate_lines.append(line.strip()) certificate_lines.append(line.strip())
return base64.b64decode(''.join(certificate_lines)) return base64.b64decode("".join(certificate_lines))
def process_links(info, callback): def process_links(info, callback):
''' """
Process link header, calls callback for every link header with the URL and relation as options. Process link header, calls callback for every link header with the URL and relation as options.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
''' """
if 'link' in info: if "link" in info:
link = info['link'] link = info["link"]
for url, relation in re.findall(r'<([^>]+)>;\s*rel="(\w+)"', link): for url, relation in re.findall(r'<([^>]+)>;\s*rel="(\w+)"', link):
callback(unquote(url), relation) callback(unquote(url), relation)
def parse_retry_after(value, relative_with_timezone=True, now=None): def parse_retry_after(value, relative_with_timezone=True, now=None):
''' """
Parse the value of a Retry-After header and return a timestamp. Parse the value of a Retry-After header and return a timestamp.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
''' """
# First try a number of seconds # First try a number of seconds
try: try:
delta = datetime.timedelta(seconds=int(value)) delta = datetime.timedelta(seconds=int(value))
@@ -95,11 +106,11 @@ def parse_retry_after(value, relative_with_timezone=True, now=None):
pass pass
try: try:
return datetime.datetime.strptime(value, '%a, %d %b %Y %H:%M:%S GMT') return datetime.datetime.strptime(value, "%a, %d %b %Y %H:%M:%S GMT")
except ValueError: except ValueError:
pass pass
raise ValueError('Cannot parse Retry-After header value %s' % repr(value)) raise ValueError("Cannot parse Retry-After header value %s" % repr(value))
def compute_cert_id( def compute_cert_id(
@@ -111,20 +122,26 @@ def compute_cert_id(
): ):
# Obtain certificate info if not provided # Obtain certificate info if not provided
if cert_info is None: if cert_info is None:
cert_info = backend.get_cert_information(cert_filename=cert_filename, cert_content=cert_content) cert_info = backend.get_cert_information(
cert_filename=cert_filename, cert_content=cert_content
)
# Convert Authority Key Identifier to string # Convert Authority Key Identifier to string
if cert_info.authority_key_identifier is None: if cert_info.authority_key_identifier is None:
if none_if_required_information_is_missing: if none_if_required_information_is_missing:
return None return None
raise ModuleFailException('Certificate has no Authority Key Identifier extension') raise ModuleFailException(
aki = to_native(base64.urlsafe_b64encode(cert_info.authority_key_identifier)).replace('=', '') "Certificate has no Authority Key Identifier extension"
)
aki = to_native(
base64.urlsafe_b64encode(cert_info.authority_key_identifier)
).replace("=", "")
# Convert serial number to string # Convert serial number to string
serial_bytes = convert_int_to_bytes(cert_info.serial_number) serial_bytes = convert_int_to_bytes(cert_info.serial_number)
if ord(serial_bytes[:1]) >= 128: if ord(serial_bytes[:1]) >= 128:
serial_bytes = b'\x00' + serial_bytes serial_bytes = b"\x00" + serial_bytes
serial = to_native(base64.urlsafe_b64encode(serial_bytes)).replace('=', '') serial = to_native(base64.urlsafe_b64encode(serial_bytes)).replace("=", "")
# Compose cert ID # Compose cert ID
return '{aki}.{serial}'.format(aki=aki, serial=serial) return "{aki}.{serial}".format(aki=aki, serial=serial)

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -18,7 +20,15 @@ def _ensure_list(value):
class ArgumentSpec: class ArgumentSpec:
def __init__(self, argument_spec=None, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None): def __init__(
self,
argument_spec=None,
mutually_exclusive=None,
required_together=None,
required_one_of=None,
required_if=None,
required_by=None,
):
self.argument_spec = argument_spec or {} self.argument_spec = argument_spec or {}
self.mutually_exclusive = _ensure_list(mutually_exclusive) self.mutually_exclusive = _ensure_list(mutually_exclusive)
self.required_together = _ensure_list(required_together) self.required_together = _ensure_list(required_together)
@@ -30,7 +40,14 @@ class ArgumentSpec:
self.argument_spec.update(kwargs) self.argument_spec.update(kwargs)
return self return self
def update(self, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None): def update(
self,
mutually_exclusive=None,
required_together=None,
required_one_of=None,
required_if=None,
required_by=None,
):
if mutually_exclusive: if mutually_exclusive:
self.mutually_exclusive.extend(mutually_exclusive) self.mutually_exclusive.extend(mutually_exclusive)
if required_together: if required_together:
@@ -66,10 +83,11 @@ class ArgumentSpec:
required_one_of=self.required_one_of, required_one_of=self.required_one_of,
required_if=self.required_if, required_if=self.required_if,
required_by=self.required_by, required_by=self.required_by,
**kwargs) **kwargs
)
def create_ansible_module(self, **kwargs): def create_ansible_module(self, **kwargs):
return self.create_ansible_module_helper(AnsibleModule, (), **kwargs) return self.create_ansible_module_helper(AnsibleModule, (), **kwargs)
__all__ = ('ArgumentSpec', ) __all__ = ("ArgumentSpec",)

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import re import re
@@ -29,8 +31,10 @@ type:
value: value:
The value to encode, the format of this value depends on the <type> specified. The value to encode, the format of this value depends on the <type> specified.
""" """
ASN1_STRING_REGEX = re.compile(r'^((?P<tag_type>IMPLICIT|EXPLICIT):(?P<tag_number>\d+)(?P<tag_class>U|A|P|C)?,)?' ASN1_STRING_REGEX = re.compile(
r'(?P<value_type>[\w\d]+):(?P<value>.*)') r"^((?P<tag_type>IMPLICIT|EXPLICIT):(?P<tag_number>\d+)(?P<tag_class>U|A|P|C)?,)?"
r"(?P<value_type>[\w\d]+):(?P<value>.*)"
)
class TagClass: class TagClass:
@@ -46,7 +50,7 @@ class TagNumber:
def _pack_octet_integer(value): def _pack_octet_integer(value):
""" Packs an integer value into 1 or multiple octets. """ """Packs an integer value into 1 or multiple octets."""
# NOTE: This is *NOT* the same as packing an ASN.1 INTEGER like value. # NOTE: This is *NOT* the same as packing an ASN.1 INTEGER like value.
octets = bytearray() octets = bytearray()
@@ -68,37 +72,41 @@ def _pack_octet_integer(value):
def serialize_asn1_string_as_der(value): def serialize_asn1_string_as_der(value):
""" Deserializes an ASN.1 string to a DER encoded byte string. """ """Deserializes an ASN.1 string to a DER encoded byte string."""
asn1_match = ASN1_STRING_REGEX.match(value) asn1_match = ASN1_STRING_REGEX.match(value)
if not asn1_match: if not asn1_match:
raise ValueError("The ASN.1 serialized string must be in the format [modifier,]type[:value]") raise ValueError(
"The ASN.1 serialized string must be in the format [modifier,]type[:value]"
)
tag_type = asn1_match.group('tag_type') tag_type = asn1_match.group("tag_type")
tag_number = asn1_match.group('tag_number') tag_number = asn1_match.group("tag_number")
tag_class = asn1_match.group('tag_class') or 'C' tag_class = asn1_match.group("tag_class") or "C"
value_type = asn1_match.group('value_type') value_type = asn1_match.group("value_type")
asn1_value = asn1_match.group('value') asn1_value = asn1_match.group("value")
if value_type != 'UTF8': if value_type != "UTF8":
raise ValueError('The ASN.1 serialized string is not a known type "{0}", only UTF8 types are ' raise ValueError(
'supported'.format(value_type)) 'The ASN.1 serialized string is not a known type "{0}", only UTF8 types are '
"supported".format(value_type)
)
b_value = to_bytes(asn1_value, encoding='utf-8', errors='surrogate_or_strict') b_value = to_bytes(asn1_value, encoding="utf-8", errors="surrogate_or_strict")
# We should only do a universal type tag if not IMPLICITLY tagged or the tag class is not universal. # We should only do a universal type tag if not IMPLICITLY tagged or the tag class is not universal.
if not tag_type or (tag_type == 'EXPLICIT' and tag_class != 'U'): if not tag_type or (tag_type == "EXPLICIT" and tag_class != "U"):
b_value = pack_asn1(TagClass.universal, False, TagNumber.utf8_string, b_value) b_value = pack_asn1(TagClass.universal, False, TagNumber.utf8_string, b_value)
if tag_type: if tag_type:
tag_class = { tag_class = {
'U': TagClass.universal, "U": TagClass.universal,
'A': TagClass.application, "A": TagClass.application,
'P': TagClass.private, "P": TagClass.private,
'C': TagClass.context_specific, "C": TagClass.context_specific,
}[tag_class] }[tag_class]
# When adding support for more types this should be looked into further. For now it works with UTF8Strings. # When adding support for more types this should be looked into further. For now it works with UTF8Strings.
constructed = tag_type == 'EXPLICIT' and tag_class != TagClass.universal constructed = tag_type == "EXPLICIT" and tag_class != TagClass.universal
b_value = pack_asn1(tag_class, constructed, int(tag_number), b_value) b_value = pack_asn1(tag_class, constructed, int(tag_number), b_value)
return b_value return b_value
@@ -119,7 +127,7 @@ def pack_asn1(tag_class, constructed, tag_number, b_data):
# Bit 8 and 7 denotes the class. # Bit 8 and 7 denotes the class.
identifier_octets = tag_class << 6 identifier_octets = tag_class << 6
# Bit 6 denotes whether the value is primitive or constructed. # Bit 6 denotes whether the value is primitive or constructed.
identifier_octets |= ((1 if constructed else 0) << 5) identifier_octets |= (1 if constructed else 0) << 5
# Bits 5-1 contain the tag number, if it cannot be encoded in these 5 bits # Bits 5-1 contain the tag number, if it cannot be encoded in these 5 bits
# then they are set and another octet(s) is used to denote the tag number. # then they are set and another octet(s) is used to denote the tag number.

View File

@@ -27,6 +27,8 @@
# pyca/cryptography@d607dd7e5bc5c08854ec0c9baff70ba4a35be36f # pyca/cryptography@d607dd7e5bc5c08854ec0c9baff70ba4a35be36f
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -34,6 +36,7 @@ __metaclass__ = type
# It must **ONLY** be used in compatibility code for older # It must **ONLY** be used in compatibility code for older
# cryptography versions! # 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

View File

@@ -5,11 +5,14 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from ._objects_data import OID_MAP from ._objects_data import OID_MAP
OID_LOOKUP = dict() OID_LOOKUP = dict()
NORMALIZE_NAMES = dict() NORMALIZE_NAMES = dict()
NORMALIZE_NAMES_SHORT = dict() NORMALIZE_NAMES_SHORT = dict()
@@ -18,17 +21,19 @@ for dotted, names in OID_MAP.items():
for name in names: for name in names:
if name in NORMALIZE_NAMES and OID_LOOKUP[name] != dotted: if name in NORMALIZE_NAMES and OID_LOOKUP[name] != dotted:
raise AssertionError( raise AssertionError(
'Name collision during setup: "{0}" for OIDs {1} and {2}' 'Name collision during setup: "{0}" for OIDs {1} and {2}'.format(
.format(name, dotted, OID_LOOKUP[name]) name, dotted, OID_LOOKUP[name]
)
) )
NORMALIZE_NAMES[name] = names[0] NORMALIZE_NAMES[name] = names[0]
NORMALIZE_NAMES_SHORT[name] = names[-1] NORMALIZE_NAMES_SHORT[name] = names[-1]
OID_LOOKUP[name] = dotted OID_LOOKUP[name] = dotted
for alias, original in [('userID', 'userId')]: for alias, original in [("userID", "userId")]:
if alias in NORMALIZE_NAMES: if alias in NORMALIZE_NAMES:
raise AssertionError( raise AssertionError(
'Name collision during adding aliases: "{0}" (alias for "{1}") is already mapped to OID {2}' 'Name collision during adding aliases: "{0}" (alias for "{1}") is already mapped to OID {2}'.format(
.format(alias, original, OID_LOOKUP[alias]) alias, original, OID_LOOKUP[alias]
)
) )
NORMALIZE_NAMES[alias] = original NORMALIZE_NAMES[alias] = original
NORMALIZE_NAMES_SHORT[alias] = NORMALIZE_NAMES_SHORT[original] NORMALIZE_NAMES_SHORT[alias] = NORMALIZE_NAMES_SHORT[original]

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,15 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
try: try:
import cryptography import cryptography
@@ -22,7 +27,7 @@ try:
# actually doing that in x509_certificate, and potentially in other code, # actually doing that in x509_certificate, and potentially in other code,
# we need to monkey-patch __hash__ for these classes to make sure our code # we need to monkey-patch __hash__ for these classes to make sure our code
# works fine. # works fine.
if LooseVersion(cryptography.__version__) < LooseVersion('2.1'): if LooseVersion(cryptography.__version__) < LooseVersion("2.1"):
# A very simply hash function which relies on the representation # A very simply hash function which relies on the representation
# of an object to be implemented. This is the case since at least # of an object to be implemented. This is the case since at least
# cryptography 1.0, see # cryptography 1.0, see
@@ -39,7 +44,7 @@ try:
x509.OtherName.__hash__ = simple_hash x509.OtherName.__hash__ = simple_hash
x509.RegisteredID.__hash__ = simple_hash x509.RegisteredID.__hash__ = simple_hash
if LooseVersion(cryptography.__version__) < LooseVersion('1.2'): if LooseVersion(cryptography.__version__) < LooseVersion("1.2"):
# The hash functions for the following types were added for cryptography 1.2: # The hash functions for the following types were added for cryptography 1.2:
# https://github.com/pyca/cryptography/commit/b642deed88a8696e5f01ce6855ccf89985fc35d0 # https://github.com/pyca/cryptography/commit/b642deed88a8696e5f01ce6855ccf89985fc35d0
# https://github.com/pyca/cryptography/commit/d1b5681f6db2bde7a14625538bd7907b08dfb486 # https://github.com/pyca/cryptography/commit/d1b5681f6db2bde7a14625538bd7907b08dfb486
@@ -50,6 +55,7 @@ try:
try: try:
# added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/dsa/ # added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/dsa/
import cryptography.hazmat.primitives.asymmetric.dsa import cryptography.hazmat.primitives.asymmetric.dsa
CRYPTOGRAPHY_HAS_DSA = True CRYPTOGRAPHY_HAS_DSA = True
try: try:
# added later in 1.5 # added later in 1.5
@@ -63,6 +69,7 @@ try:
try: try:
# added in 2.6 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed25519/ # added in 2.6 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed25519/
import cryptography.hazmat.primitives.asymmetric.ed25519 import cryptography.hazmat.primitives.asymmetric.ed25519
CRYPTOGRAPHY_HAS_ED25519 = True CRYPTOGRAPHY_HAS_ED25519 = True
try: try:
# added with the primitive in 2.6 # added with the primitive in 2.6
@@ -76,6 +83,7 @@ try:
try: try:
# added in 2.6 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed448/ # added in 2.6 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed448/
import cryptography.hazmat.primitives.asymmetric.ed448 import cryptography.hazmat.primitives.asymmetric.ed448
CRYPTOGRAPHY_HAS_ED448 = True CRYPTOGRAPHY_HAS_ED448 = True
try: try:
# added with the primitive in 2.6 # added with the primitive in 2.6
@@ -89,6 +97,7 @@ try:
try: try:
# added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/ # added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
import cryptography.hazmat.primitives.asymmetric.ec import cryptography.hazmat.primitives.asymmetric.ec
CRYPTOGRAPHY_HAS_EC = True CRYPTOGRAPHY_HAS_EC = True
try: try:
# added later in 1.5 # added later in 1.5
@@ -102,6 +111,7 @@ try:
try: try:
# added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/ # added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/
import cryptography.hazmat.primitives.asymmetric.rsa import cryptography.hazmat.primitives.asymmetric.rsa
CRYPTOGRAPHY_HAS_RSA = True CRYPTOGRAPHY_HAS_RSA = True
try: try:
# added later in 1.4 # added later in 1.4
@@ -115,6 +125,7 @@ try:
try: try:
# added in 2.0 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x25519/ # added in 2.0 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x25519/
import cryptography.hazmat.primitives.asymmetric.x25519 import cryptography.hazmat.primitives.asymmetric.x25519
CRYPTOGRAPHY_HAS_X25519 = True CRYPTOGRAPHY_HAS_X25519 = True
try: try:
# added later in 2.5 # added later in 2.5
@@ -128,6 +139,7 @@ try:
try: try:
# added in 2.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x448/ # added in 2.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x448/
import cryptography.hazmat.primitives.asymmetric.x448 import cryptography.hazmat.primitives.asymmetric.x448
CRYPTOGRAPHY_HAS_X448 = True CRYPTOGRAPHY_HAS_X448 = True
except ImportError: except ImportError:
CRYPTOGRAPHY_HAS_X448 = False CRYPTOGRAPHY_HAS_X448 = False

View File

@@ -5,10 +5,15 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion as _LooseVersion from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion as _LooseVersion,
)
try: try:
import cryptography import cryptography
@@ -17,18 +22,9 @@ except ImportError:
# Error handled in the calling module. # Error handled in the calling module.
pass pass
from .basic import ( from ._obj2txt import obj2txt
HAS_CRYPTOGRAPHY, from .basic import HAS_CRYPTOGRAPHY
) from .cryptography_support import CRYPTOGRAPHY_TIMEZONE, cryptography_decode_name
from .cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_decode_name,
)
from ._obj2txt import (
obj2txt,
)
# TODO: once cryptography has a _utc variant of InvalidityDate.invalidity_date, set this # TODO: once cryptography has a _utc variant of InvalidityDate.invalidity_date, set this
@@ -36,23 +32,25 @@ from ._obj2txt import (
# (https://github.com/pyca/cryptography/issues/10818) # (https://github.com/pyca/cryptography/issues/10818)
CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE = False CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE = False
if HAS_CRYPTOGRAPHY: if HAS_CRYPTOGRAPHY:
CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE = _LooseVersion(cryptography.__version__) >= _LooseVersion('43.0.0') CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE = _LooseVersion(
cryptography.__version__
) >= _LooseVersion("43.0.0")
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
if HAS_CRYPTOGRAPHY: if HAS_CRYPTOGRAPHY:
REVOCATION_REASON_MAP = { REVOCATION_REASON_MAP = {
'unspecified': x509.ReasonFlags.unspecified, "unspecified": x509.ReasonFlags.unspecified,
'key_compromise': x509.ReasonFlags.key_compromise, "key_compromise": x509.ReasonFlags.key_compromise,
'ca_compromise': x509.ReasonFlags.ca_compromise, "ca_compromise": x509.ReasonFlags.ca_compromise,
'affiliation_changed': x509.ReasonFlags.affiliation_changed, "affiliation_changed": x509.ReasonFlags.affiliation_changed,
'superseded': x509.ReasonFlags.superseded, "superseded": x509.ReasonFlags.superseded,
'cessation_of_operation': x509.ReasonFlags.cessation_of_operation, "cessation_of_operation": x509.ReasonFlags.cessation_of_operation,
'certificate_hold': x509.ReasonFlags.certificate_hold, "certificate_hold": x509.ReasonFlags.certificate_hold,
'privilege_withdrawn': x509.ReasonFlags.privilege_withdrawn, "privilege_withdrawn": x509.ReasonFlags.privilege_withdrawn,
'aa_compromise': x509.ReasonFlags.aa_compromise, "aa_compromise": x509.ReasonFlags.aa_compromise,
'remove_from_crl': x509.ReasonFlags.remove_from_crl, "remove_from_crl": x509.ReasonFlags.remove_from_crl,
} }
REVOCATION_REASON_MAP_INVERSE = dict() REVOCATION_REASON_MAP_INVERSE = dict()
for k, v in REVOCATION_REASON_MAP.items(): for k, v in REVOCATION_REASON_MAP.items():
@@ -65,50 +63,61 @@ else:
def cryptography_decode_revoked_certificate(cert): def cryptography_decode_revoked_certificate(cert):
result = { result = {
'serial_number': cert.serial_number, "serial_number": cert.serial_number,
'revocation_date': get_revocation_date(cert), "revocation_date": get_revocation_date(cert),
'issuer': None, "issuer": None,
'issuer_critical': False, "issuer_critical": False,
'reason': None, "reason": None,
'reason_critical': False, "reason_critical": False,
'invalidity_date': None, "invalidity_date": None,
'invalidity_date_critical': False, "invalidity_date_critical": False,
} }
try: try:
ext = cert.extensions.get_extension_for_class(x509.CertificateIssuer) ext = cert.extensions.get_extension_for_class(x509.CertificateIssuer)
result['issuer'] = list(ext.value) result["issuer"] = list(ext.value)
result['issuer_critical'] = ext.critical result["issuer_critical"] = ext.critical
except x509.ExtensionNotFound: except x509.ExtensionNotFound:
pass pass
try: try:
ext = cert.extensions.get_extension_for_class(x509.CRLReason) ext = cert.extensions.get_extension_for_class(x509.CRLReason)
result['reason'] = ext.value.reason result["reason"] = ext.value.reason
result['reason_critical'] = ext.critical result["reason_critical"] = ext.critical
except x509.ExtensionNotFound: except x509.ExtensionNotFound:
pass pass
try: try:
ext = cert.extensions.get_extension_for_class(x509.InvalidityDate) ext = cert.extensions.get_extension_for_class(x509.InvalidityDate)
result['invalidity_date'] = get_invalidity_date(ext.value) result["invalidity_date"] = get_invalidity_date(ext.value)
result['invalidity_date_critical'] = ext.critical result["invalidity_date_critical"] = ext.critical
except x509.ExtensionNotFound: except x509.ExtensionNotFound:
pass pass
return result return result
def cryptography_dump_revoked(entry, idn_rewrite='ignore'): def cryptography_dump_revoked(entry, idn_rewrite="ignore"):
return { return {
'serial_number': entry['serial_number'], "serial_number": entry["serial_number"],
'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT), "revocation_date": entry["revocation_date"].strftime(TIMESTAMP_FORMAT),
'issuer': "issuer": (
[cryptography_decode_name(issuer, idn_rewrite=idn_rewrite) for issuer in entry['issuer']] [
if entry['issuer'] is not None else None, cryptography_decode_name(issuer, idn_rewrite=idn_rewrite)
'issuer_critical': entry['issuer_critical'], for issuer in entry["issuer"]
'reason': REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None, ]
'reason_critical': entry['reason_critical'], if entry["issuer"] is not None
'invalidity_date': else None
entry['invalidity_date'].strftime(TIMESTAMP_FORMAT) ),
if entry['invalidity_date'] is not None else None, "issuer_critical": entry["issuer_critical"],
'invalidity_date_critical': entry['invalidity_date_critical'], "reason": (
REVOCATION_REASON_MAP_INVERSE.get(entry["reason"])
if entry["reason"] is not None
else None
),
"reason_critical": entry["reason_critical"],
"invalidity_date": (
entry["invalidity_date"].strftime(TIMESTAMP_FORMAT)
if entry["invalidity_date"] is not None
else None
),
"invalidity_date_critical": entry["invalidity_date_critical"],
} }
@@ -118,9 +127,7 @@ def cryptography_get_signature_algorithm_oid_from_crl(crl):
except AttributeError: except AttributeError:
# Older cryptography versions do not have signature_algorithm_oid yet # Older cryptography versions do not have signature_algorithm_oid yet
dotted = obj2txt( dotted = obj2txt(
crl._backend._lib, crl._backend._lib, crl._backend._ffi, crl._x509_crl.sig_alg.algorithm
crl._backend._ffi,
crl._x509_crl.sig_alg.algorithm
) )
return x509.oid.ObjectIdentifier(dotted) return x509.oid.ObjectIdentifier(dotted)

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -14,21 +16,29 @@ import re
import sys import sys
import traceback import traceback
from ansible.module_utils.common.text.converters import to_text, to_bytes, to_native from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlunparse, ParseResult from ansible.module_utils.six.moves.urllib.parse import (
ParseResult,
urlparse,
urlunparse,
)
from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
from ._asn1 import serialize_asn1_string_as_der from ._asn1 import serialize_asn1_string_as_der
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
try: try:
import ipaddress
import cryptography import cryptography
from cryptography import x509 from cryptography import x509
from cryptography.exceptions import InvalidSignature from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import padding
import ipaddress
_HAS_CRYPTOGRAPHY = True _HAS_CRYPTOGRAPHY = True
except ImportError: except ImportError:
_HAS_CRYPTOGRAPHY = False _HAS_CRYPTOGRAPHY = False
@@ -84,36 +94,31 @@ except ImportError:
from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.basic import missing_required_lib
from ._obj2txt import obj2txt
from ._objects import NORMALIZE_NAMES, NORMALIZE_NAMES_SHORT, OID_LOOKUP, OID_MAP
from .basic import ( from .basic import (
CRYPTOGRAPHY_HAS_DSA_SIGN, CRYPTOGRAPHY_HAS_DSA_SIGN,
CRYPTOGRAPHY_HAS_EC_SIGN, CRYPTOGRAPHY_HAS_EC_SIGN,
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_ED25519_SIGN,
CRYPTOGRAPHY_HAS_ED448, CRYPTOGRAPHY_HAS_ED448,
CRYPTOGRAPHY_HAS_ED448_SIGN, CRYPTOGRAPHY_HAS_ED448_SIGN,
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_ED25519_SIGN,
CRYPTOGRAPHY_HAS_RSA_SIGN, CRYPTOGRAPHY_HAS_RSA_SIGN,
CRYPTOGRAPHY_HAS_X448,
CRYPTOGRAPHY_HAS_X25519, CRYPTOGRAPHY_HAS_X25519,
CRYPTOGRAPHY_HAS_X25519_FULL, CRYPTOGRAPHY_HAS_X25519_FULL,
CRYPTOGRAPHY_HAS_X448,
OpenSSLObjectError, OpenSSLObjectError,
) )
from ._objects import (
OID_LOOKUP,
OID_MAP,
NORMALIZE_NAMES_SHORT,
NORMALIZE_NAMES,
)
from ._obj2txt import obj2txt
CRYPTOGRAPHY_TIMEZONE = False CRYPTOGRAPHY_TIMEZONE = False
if _HAS_CRYPTOGRAPHY: if _HAS_CRYPTOGRAPHY:
CRYPTOGRAPHY_TIMEZONE = LooseVersion(cryptography.__version__) >= LooseVersion('42.0.0') CRYPTOGRAPHY_TIMEZONE = LooseVersion(cryptography.__version__) >= LooseVersion(
"42.0.0"
)
DOTTED_OID = re.compile(r'^\d+(?:\.\d+)+$') DOTTED_OID = re.compile(r"^\d+(?:\.\d+)+$")
def cryptography_get_extensions_from_cert(cert): def cryptography_get_extensions_from_cert(cert):
@@ -148,7 +153,11 @@ def cryptography_get_extensions_from_cert(cert):
value=to_native(base64.b64encode(der)), value=to_native(base64.b64encode(der)),
) )
try: try:
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext)) oid = obj2txt(
backend._lib,
backend._ffi,
backend._lib.X509_EXTENSION_get_object(ext),
)
except AttributeError: except AttributeError:
oid = exts[i].oid.dotted_string oid = exts[i].oid.dotted_string
result[oid] = entry result[oid] = entry
@@ -187,8 +196,10 @@ def cryptography_get_extensions_from_csr(csr):
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"
),
),
) )
# With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does # With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does
@@ -208,7 +219,11 @@ def cryptography_get_extensions_from_csr(csr):
value=to_native(base64.b64encode(der)), value=to_native(base64.b64encode(der)),
) )
try: try:
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext)) oid = obj2txt(
backend._lib,
backend._ffi,
backend._lib.X509_EXTENSION_get_object(ext),
)
except AttributeError: except AttributeError:
oid = exts[i].oid.dotted_string oid = exts[i].oid.dotted_string
result[oid] = entry result[oid] = entry
@@ -244,7 +259,7 @@ def cryptography_oid_to_name(oid, short=False):
name = names[0] name = names[0]
else: else:
name = oid._name name = oid._name
if name == 'Unknown OID': if name == "Unknown OID":
name = dotted_string name = dotted_string
if short: if short:
return NORMALIZE_NAMES_SHORT.get(name, name) return NORMALIZE_NAMES_SHORT.get(name, name)
@@ -256,104 +271,128 @@ def _get_hex(bytesstr):
if bytesstr is None: if bytesstr is None:
return bytesstr return bytesstr
data = binascii.hexlify(bytesstr) data = binascii.hexlify(bytesstr)
data = to_text(b':'.join(data[i:i + 2] for i in range(0, len(data), 2))) data = to_text(b":".join(data[i : i + 2] for i in range(0, len(data), 2)))
return data return data
def _parse_hex(bytesstr): def _parse_hex(bytesstr):
if bytesstr is None: if bytesstr is None:
return bytesstr return bytesstr
data = ''.join([('0' * (2 - len(p)) + p) if len(p) < 2 else p for p in to_text(bytesstr).split(':')]) data = "".join(
[
("0" * (2 - len(p)) + p) if len(p) < 2 else p
for p in to_text(bytesstr).split(":")
]
)
data = binascii.unhexlify(data) data = binascii.unhexlify(data)
return data return data
DN_COMPONENT_START_RE = re.compile(b'^ *([a-zA-z0-9.]+) *= *') DN_COMPONENT_START_RE = re.compile(b"^ *([a-zA-z0-9.]+) *= *")
DN_HEX_LETTER = b'0123456789abcdef' DN_HEX_LETTER = b"0123456789abcdef"
if sys.version_info[0] < 3: if sys.version_info[0] < 3:
_int_to_byte = chr _int_to_byte = chr
else: else:
def _int_to_byte(value): def _int_to_byte(value):
return bytes((value, )) return bytes((value,))
def _parse_dn_component(name, sep=b',', decode_remainder=True): def _parse_dn_component(name, sep=b",", decode_remainder=True):
m = DN_COMPONENT_START_RE.match(name) m = DN_COMPONENT_START_RE.match(name)
if not m: if not m:
raise OpenSSLObjectError(u'cannot start part in "{0}"'.format(to_text(name))) raise OpenSSLObjectError(u'cannot start part in "{0}"'.format(to_text(name)))
oid = cryptography_name_to_oid(to_text(m.group(1))) oid = cryptography_name_to_oid(to_text(m.group(1)))
idx = len(m.group(0)) idx = len(m.group(0))
decoded_name = [] decoded_name = []
sep_str = sep + b'\\' sep_str = sep + b"\\"
if decode_remainder: if decode_remainder:
length = len(name) length = len(name)
if length > idx and name[idx:idx + 1] == b'#': if length > idx and name[idx : idx + 1] == b"#":
# Decoding a hex string # Decoding a hex string
idx += 1 idx += 1
while idx + 1 < length: while idx + 1 < length:
ch1 = name[idx:idx + 1] ch1 = name[idx : idx + 1]
ch2 = name[idx + 1:idx + 2] ch2 = name[idx + 1 : idx + 2]
idx1 = DN_HEX_LETTER.find(ch1.lower()) idx1 = DN_HEX_LETTER.find(ch1.lower())
idx2 = DN_HEX_LETTER.find(ch2.lower()) idx2 = DN_HEX_LETTER.find(ch2.lower())
if idx1 < 0 or idx2 < 0: if idx1 < 0 or idx2 < 0:
raise OpenSSLObjectError(u'Invalid hex sequence entry "{0}"'.format(to_text(ch1 + ch2))) raise OpenSSLObjectError(
u'Invalid hex sequence entry "{0}"'.format(to_text(ch1 + ch2))
)
idx += 2 idx += 2
decoded_name.append(_int_to_byte(idx1 * 16 + idx2)) decoded_name.append(_int_to_byte(idx1 * 16 + idx2))
else: else:
# Decoding a regular string # Decoding a regular string
while idx < length: while idx < length:
i = idx i = idx
while i < length and name[i:i + 1] not in sep_str: while i < length and name[i : i + 1] not in sep_str:
i += 1 i += 1
if i > idx: if i > idx:
decoded_name.append(name[idx:i]) decoded_name.append(name[idx:i])
idx = i idx = i
while idx + 1 < length and name[idx:idx + 1] == b'\\': while idx + 1 < length and name[idx : idx + 1] == b"\\":
ch = name[idx + 1:idx + 2] ch = name[idx + 1 : idx + 2]
idx1 = DN_HEX_LETTER.find(ch.lower()) idx1 = DN_HEX_LETTER.find(ch.lower())
if idx1 >= 0: if idx1 >= 0:
if idx + 2 >= length: if idx + 2 >= length:
raise OpenSSLObjectError(u'Hex escape sequence "\\{0}" incomplete at end of string'.format(to_text(ch))) raise OpenSSLObjectError(
ch2 = name[idx + 2:idx + 3] u'Hex escape sequence "\\{0}" incomplete at end of string'.format(
to_text(ch)
)
)
ch2 = name[idx + 2 : idx + 3]
idx2 = DN_HEX_LETTER.find(ch2.lower()) idx2 = DN_HEX_LETTER.find(ch2.lower())
if idx2 < 0: if idx2 < 0:
raise OpenSSLObjectError(u'Hex escape sequence "\\{0}" has invalid second letter'.format(to_text(ch + ch2))) raise OpenSSLObjectError(
u'Hex escape sequence "\\{0}" has invalid second letter'.format(
to_text(ch + ch2)
)
)
ch = _int_to_byte(idx1 * 16 + idx2) ch = _int_to_byte(idx1 * 16 + idx2)
idx += 1 idx += 1
idx += 2 idx += 2
decoded_name.append(ch) decoded_name.append(ch)
if idx < length and name[idx:idx + 1] == sep: if idx < length and name[idx : idx + 1] == sep:
break break
else: else:
decoded_name.append(name[idx:]) decoded_name.append(name[idx:])
idx = len(name) idx = len(name)
return x509.NameAttribute(oid, to_text(b''.join(decoded_name))), name[idx:] return x509.NameAttribute(oid, to_text(b"".join(decoded_name))), name[idx:]
def _parse_dn(name): def _parse_dn(name):
''' """
Parse a Distinguished Name. Parse a Distinguished Name.
Can be of the form ``CN=Test, O = Something`` or ``CN = Test,O= Something``. Can be of the form ``CN=Test, O = Something`` or ``CN = Test,O= Something``.
''' """
original_name = name original_name = name
name = name.lstrip() name = name.lstrip()
sep = b',' sep = b","
if name.startswith(b'/'): if name.startswith(b"/"):
sep = b'/' sep = b"/"
name = name[1:] name = name[1:]
result = [] result = []
while name: while name:
try: try:
attribute, name = _parse_dn_component(name, sep=sep) attribute, name = _parse_dn_component(name, sep=sep)
except OpenSSLObjectError as e: except OpenSSLObjectError as e:
raise OpenSSLObjectError(u'Error while parsing distinguished name "{0}": {1}'.format(to_text(original_name), e)) raise OpenSSLObjectError(
u'Error while parsing distinguished name "{0}": {1}'.format(
to_text(original_name), e
)
)
result.append(attribute) result.append(attribute)
if name: if name:
if name[0:1] != sep or len(name) < 2: if name[0:1] != sep or len(name) < 2:
raise OpenSSLObjectError(u'Error while parsing distinguished name "{0}": unexpected end of string'.format(to_text(original_name))) raise OpenSSLObjectError(
u'Error while parsing distinguished name "{0}": unexpected end of string'.format(
to_text(original_name)
)
)
name = name[1:] name = name[1:]
return result return result
@@ -364,12 +403,16 @@ def cryptography_parse_relative_distinguished_name(rdn):
try: try:
names.append(_parse_dn_component(to_bytes(part), decode_remainder=False)[0]) names.append(_parse_dn_component(to_bytes(part), decode_remainder=False)[0])
except OpenSSLObjectError as e: except OpenSSLObjectError as e:
raise OpenSSLObjectError(u'Error while parsing relative distinguished name "{0}": {1}'.format(part, e)) raise OpenSSLObjectError(
u'Error while parsing relative distinguished name "{0}": {1}'.format(
part, e
)
)
return cryptography.x509.RelativeDistinguishedName(names) return cryptography.x509.RelativeDistinguishedName(names)
def _is_ascii(value): def _is_ascii(value):
'''Check whether the Unicode string `value` contains only ASCII characters.''' """Check whether the Unicode string `value` contains only ASCII characters."""
try: try:
value.encode("ascii") value.encode("ascii")
return True return True
@@ -378,195 +421,244 @@ def _is_ascii(value):
def _adjust_idn(value, idn_rewrite): def _adjust_idn(value, idn_rewrite):
if idn_rewrite == 'ignore' or not value: if idn_rewrite == "ignore" or not value:
return value return value
if idn_rewrite == 'idna' and _is_ascii(value): if idn_rewrite == "idna" and _is_ascii(value):
return value return value
if idn_rewrite not in ('idna', 'unicode'): if idn_rewrite not in ("idna", "unicode"):
raise ValueError('Invalid value for idn_rewrite: "{0}"'.format(idn_rewrite)) raise ValueError('Invalid value for idn_rewrite: "{0}"'.format(idn_rewrite))
if not HAS_IDNA: if not HAS_IDNA:
raise OpenSSLObjectError( raise OpenSSLObjectError(
missing_required_lib('idna', reason='to transform {what} DNS name "{name}" to {dest}'.format( missing_required_lib(
"idna",
reason='to transform {what} DNS name "{name}" to {dest}'.format(
name=value, name=value,
what='IDNA' if idn_rewrite == 'unicode' else 'Unicode', what="IDNA" if idn_rewrite == "unicode" else "Unicode",
dest='Unicode' if idn_rewrite == 'unicode' else 'IDNA', dest="Unicode" if idn_rewrite == "unicode" else "IDNA",
))) ),
)
)
# Since IDNA does not like '*' or empty labels (except one empty label at the end), # Since IDNA does not like '*' or empty labels (except one empty label at the end),
# we split and let IDNA only handle labels that are neither empty or '*'. # we split and let IDNA only handle labels that are neither empty or '*'.
parts = value.split(u'.') parts = value.split(u".")
for index, part in enumerate(parts): for index, part in enumerate(parts):
if part in (u'', u'*'): if part in (u"", u"*"):
continue continue
try: try:
if idn_rewrite == 'idna': if idn_rewrite == "idna":
parts[index] = idna.encode(part).decode('ascii') parts[index] = idna.encode(part).decode("ascii")
elif idn_rewrite == 'unicode' and part.startswith(u'xn--'): elif idn_rewrite == "unicode" and part.startswith(u"xn--"):
parts[index] = idna.decode(part) parts[index] = idna.decode(part)
except idna.IDNAError as exc2008: except idna.IDNAError as exc2008:
try: try:
if idn_rewrite == 'idna': if idn_rewrite == "idna":
parts[index] = part.encode('idna').decode('ascii') parts[index] = part.encode("idna").decode("ascii")
elif idn_rewrite == 'unicode' and part.startswith(u'xn--'): elif idn_rewrite == "unicode" and part.startswith(u"xn--"):
parts[index] = part.encode('ascii').decode('idna') parts[index] = part.encode("ascii").decode("idna")
except Exception as exc2003: except Exception as exc2003:
raise OpenSSLObjectError( raise OpenSSLObjectError(
u'Error while transforming part "{part}" of {what} DNS name "{name}" to {dest}.' u'Error while transforming part "{part}" of {what} DNS name "{name}" to {dest}.'
u' IDNA2008 transformation resulted in "{exc2008}", IDNA2003 transformation resulted in "{exc2003}".'.format( u' IDNA2008 transformation resulted in "{exc2008}", IDNA2003 transformation resulted in "{exc2003}".'.format(
part=part, part=part,
name=value, name=value,
what='IDNA' if idn_rewrite == 'unicode' else 'Unicode', what="IDNA" if idn_rewrite == "unicode" else "Unicode",
dest='Unicode' if idn_rewrite == 'unicode' else 'IDNA', dest="Unicode" if idn_rewrite == "unicode" else "IDNA",
exc2003=exc2003, exc2003=exc2003,
exc2008=exc2008, exc2008=exc2008,
)) )
return u'.'.join(parts) )
return u".".join(parts)
def _adjust_idn_email(value, idn_rewrite): def _adjust_idn_email(value, idn_rewrite):
idx = value.find(u'@') idx = value.find(u"@")
if idx < 0: if idx < 0:
return value return value
return u'{0}@{1}'.format(value[:idx], _adjust_idn(value[idx + 1:], idn_rewrite)) return u"{0}@{1}".format(value[:idx], _adjust_idn(value[idx + 1 :], idn_rewrite))
def _adjust_idn_url(value, idn_rewrite): def _adjust_idn_url(value, idn_rewrite):
url = urlparse(value) url = urlparse(value)
host = _adjust_idn(url.hostname, idn_rewrite) host = _adjust_idn(url.hostname, idn_rewrite)
if url.username is not None and url.password is not None: if url.username is not None and url.password is not None:
host = u'{0}:{1}@{2}'.format(url.username, url.password, host) host = u"{0}:{1}@{2}".format(url.username, url.password, host)
elif url.username is not None: elif url.username is not None:
host = u'{0}@{1}'.format(url.username, host) host = u"{0}@{1}".format(url.username, host)
if url.port is not None: if url.port is not None:
host = u'{0}:{1}'.format(host, url.port) host = u"{0}:{1}".format(host, url.port)
return urlunparse( return urlunparse(
ParseResult(scheme=url.scheme, netloc=host, path=url.path, params=url.params, query=url.query, fragment=url.fragment)) ParseResult(
scheme=url.scheme,
netloc=host,
path=url.path,
params=url.params,
query=url.query,
fragment=url.fragment,
)
)
def cryptography_get_name(name, what='Subject Alternative Name'): def cryptography_get_name(name, what="Subject Alternative Name"):
''' """
Given a name string, returns a cryptography x509.GeneralName object. Given a name string, returns a cryptography x509.GeneralName object.
Raises an OpenSSLObjectError if the name is unknown or cannot be parsed. Raises an OpenSSLObjectError if the name is unknown or cannot be parsed.
''' """
try: try:
if name.startswith('DNS:'): if name.startswith("DNS:"):
return x509.DNSName(_adjust_idn(to_text(name[4:]), 'idna')) return x509.DNSName(_adjust_idn(to_text(name[4:]), "idna"))
if name.startswith('IP:'): if name.startswith("IP:"):
address = to_text(name[3:]) address = to_text(name[3:])
if '/' in address: if "/" in address:
return x509.IPAddress(ipaddress.ip_network(address)) return x509.IPAddress(ipaddress.ip_network(address))
return x509.IPAddress(ipaddress.ip_address(address)) return x509.IPAddress(ipaddress.ip_address(address))
if name.startswith('email:'): if name.startswith("email:"):
return x509.RFC822Name(_adjust_idn_email(to_text(name[6:]), 'idna')) return x509.RFC822Name(_adjust_idn_email(to_text(name[6:]), "idna"))
if name.startswith('URI:'): if name.startswith("URI:"):
return x509.UniformResourceIdentifier(_adjust_idn_url(to_text(name[4:]), 'idna')) return x509.UniformResourceIdentifier(
if name.startswith('RID:'): _adjust_idn_url(to_text(name[4:]), "idna")
m = re.match(r'^([0-9]+(?:\.[0-9]+)*)$', to_text(name[4:])) )
if name.startswith("RID:"):
m = re.match(r"^([0-9]+(?:\.[0-9]+)*)$", to_text(name[4:]))
if not m: if not m:
raise OpenSSLObjectError('Cannot parse {what} "{name}"'.format(name=name, what=what)) raise OpenSSLObjectError(
'Cannot parse {what} "{name}"'.format(name=name, what=what)
)
return x509.RegisteredID(x509.oid.ObjectIdentifier(m.group(1))) return x509.RegisteredID(x509.oid.ObjectIdentifier(m.group(1)))
if name.startswith('otherName:'): if name.startswith("otherName:"):
# otherName can either be a raw ASN.1 hex string or in the format that OpenSSL works with. # otherName can either be a raw ASN.1 hex string or in the format that OpenSSL works with.
m = re.match(r'^([0-9]+(?:\.[0-9]+)*);([0-9a-fA-F]{1,2}(?::[0-9a-fA-F]{1,2})*)$', to_text(name[10:])) m = re.match(
r"^([0-9]+(?:\.[0-9]+)*);([0-9a-fA-F]{1,2}(?::[0-9a-fA-F]{1,2})*)$",
to_text(name[10:]),
)
if m: if m:
return x509.OtherName(x509.oid.ObjectIdentifier(m.group(1)), _parse_hex(m.group(2))) return x509.OtherName(
x509.oid.ObjectIdentifier(m.group(1)), _parse_hex(m.group(2))
)
# See https://www.openssl.org/docs/man1.0.2/man5/x509v3_config.html - Subject Alternative Name for more # See https://www.openssl.org/docs/man1.0.2/man5/x509v3_config.html - Subject Alternative Name for more
# defailts on the format expected. # defailts on the format expected.
name = to_text(name[10:], errors='surrogate_or_strict') name = to_text(name[10:], errors="surrogate_or_strict")
if ';' not in name: if ";" not in name:
raise OpenSSLObjectError('Cannot parse {what} otherName "{name}", must be in the ' raise OpenSSLObjectError(
'Cannot parse {what} otherName "{name}", must be in the '
'format "otherName:<OID>;<ASN.1 OpenSSL Encoded String>" or ' 'format "otherName:<OID>;<ASN.1 OpenSSL Encoded String>" or '
'"otherName:<OID>;<hex string>"'.format(name=name, what=what)) '"otherName:<OID>;<hex string>"'.format(name=name, what=what)
)
oid, value = name.split(';', 1) oid, value = name.split(";", 1)
b_value = serialize_asn1_string_as_der(value) b_value = serialize_asn1_string_as_der(value)
return x509.OtherName(x509.ObjectIdentifier(oid), b_value) return x509.OtherName(x509.ObjectIdentifier(oid), b_value)
if name.startswith('dirName:'): if name.startswith("dirName:"):
return x509.DirectoryName(x509.Name(reversed(_parse_dn(to_bytes(name[8:]))))) return x509.DirectoryName(
x509.Name(reversed(_parse_dn(to_bytes(name[8:]))))
)
except Exception as e: except Exception as e:
raise OpenSSLObjectError('Cannot parse {what} "{name}": {error}'.format(name=name, what=what, error=e)) raise OpenSSLObjectError(
if ':' not in name: 'Cannot parse {what} "{name}": {error}'.format(
raise OpenSSLObjectError('Cannot parse {what} "{name}" (forgot "DNS:" prefix?)'.format(name=name, what=what)) name=name, what=what, error=e
raise OpenSSLObjectError('Cannot parse {what} "{name}" (potentially unsupported by cryptography backend)'.format(name=name, what=what)) )
)
if ":" not in name:
raise OpenSSLObjectError(
'Cannot parse {what} "{name}" (forgot "DNS:" prefix?)'.format(
name=name, what=what
)
)
raise OpenSSLObjectError(
'Cannot parse {what} "{name}" (potentially unsupported by cryptography backend)'.format(
name=name, what=what
)
)
def _dn_escape_value(value): def _dn_escape_value(value):
''' """
Escape Distinguished Name's attribute value. Escape Distinguished Name's attribute value.
''' """
value = value.replace(u'\\', u'\\\\') value = value.replace(u"\\", u"\\\\")
for ch in [u',', u'+', u'<', u'>', u';', u'"']: for ch in [u",", u"+", u"<", u">", u";", u'"']:
value = value.replace(ch, u'\\%s' % ch) value = value.replace(ch, u"\\%s" % ch)
value = value.replace(u'\0', u'\\00') value = value.replace(u"\0", u"\\00")
if value.startswith((u' ', u'#')): if value.startswith((u" ", u"#")):
value = u'\\%s' % value[0] + value[1:] value = u"\\%s" % value[0] + value[1:]
if value.endswith(u' '): if value.endswith(u" "):
value = value[:-1] + u'\\ ' value = value[:-1] + u"\\ "
return value return value
def cryptography_decode_name(name, idn_rewrite='ignore'): def cryptography_decode_name(name, idn_rewrite="ignore"):
''' """
Given a cryptography x509.GeneralName object, returns a string. Given a cryptography x509.GeneralName object, returns a string.
Raises an OpenSSLObjectError if the name is not supported. Raises an OpenSSLObjectError if the name is not supported.
''' """
if idn_rewrite not in ('ignore', 'idna', 'unicode'): if idn_rewrite not in ("ignore", "idna", "unicode"):
raise AssertionError('idn_rewrite must be one of "ignore", "idna", or "unicode"') raise AssertionError(
'idn_rewrite must be one of "ignore", "idna", or "unicode"'
)
if isinstance(name, x509.DNSName): if isinstance(name, x509.DNSName):
return u'DNS:{0}'.format(_adjust_idn(name.value, idn_rewrite)) return u"DNS:{0}".format(_adjust_idn(name.value, idn_rewrite))
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 u'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen) return u"IP:{0}/{1}".format(
return u'IP:{0}'.format(name.value.compressed) name.value.network_address.compressed, name.value.prefixlen
)
return u"IP:{0}".format(name.value.compressed)
if isinstance(name, x509.RFC822Name): if isinstance(name, x509.RFC822Name):
return u'email:{0}'.format(_adjust_idn_email(name.value, idn_rewrite)) return u"email:{0}".format(_adjust_idn_email(name.value, idn_rewrite))
if isinstance(name, x509.UniformResourceIdentifier): if isinstance(name, x509.UniformResourceIdentifier):
return u'URI:{0}'.format(_adjust_idn_url(name.value, idn_rewrite)) return u"URI:{0}".format(_adjust_idn_url(name.value, idn_rewrite))
if isinstance(name, x509.DirectoryName): if isinstance(name, x509.DirectoryName):
# According to https://datatracker.ietf.org/doc/html/rfc4514.html#section-2.1 the # According to https://datatracker.ietf.org/doc/html/rfc4514.html#section-2.1 the
# list needs to be reversed, and joined by commas # list needs to be reversed, and joined by commas
return u'dirName:' + ','.join([ return u"dirName:" + ",".join(
u'{0}={1}'.format(to_text(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 reversed(list(name.value)) for attribute in reversed(list(name.value))
]) ]
)
if isinstance(name, x509.RegisteredID): if isinstance(name, x509.RegisteredID):
return u'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 u'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))
def _cryptography_get_keyusage(usage): def _cryptography_get_keyusage(usage):
''' """
Given a key usage identifier string, returns the parameter name used by cryptography's x509.KeyUsage(). Given a key usage identifier string, returns the parameter name used by cryptography's x509.KeyUsage().
Raises an OpenSSLObjectError if the identifier is unknown. Raises an OpenSSLObjectError if the identifier is unknown.
''' """
if usage in ('Digital Signature', 'digitalSignature'): if usage in ("Digital Signature", "digitalSignature"):
return 'digital_signature' return "digital_signature"
if usage in ('Non Repudiation', 'nonRepudiation'): if usage in ("Non Repudiation", "nonRepudiation"):
return 'content_commitment' return "content_commitment"
if usage in ('Key Encipherment', 'keyEncipherment'): if usage in ("Key Encipherment", "keyEncipherment"):
return 'key_encipherment' return "key_encipherment"
if usage in ('Data Encipherment', 'dataEncipherment'): if usage in ("Data Encipherment", "dataEncipherment"):
return 'data_encipherment' return "data_encipherment"
if usage in ('Key Agreement', 'keyAgreement'): if usage in ("Key Agreement", "keyAgreement"):
return 'key_agreement' return "key_agreement"
if usage in ('Certificate Sign', 'keyCertSign'): if usage in ("Certificate Sign", "keyCertSign"):
return 'key_cert_sign' return "key_cert_sign"
if usage in ('CRL Sign', 'cRLSign'): if usage in ("CRL Sign", "cRLSign"):
return 'crl_sign' return "crl_sign"
if usage in ('Encipher Only', 'encipherOnly'): if usage in ("Encipher Only", "encipherOnly"):
return 'encipher_only' return "encipher_only"
if usage in ('Decipher Only', 'decipherOnly'): if usage in ("Decipher Only", "decipherOnly"):
return 'decipher_only' return "decipher_only"
raise OpenSSLObjectError('Unknown key usage "{0}"'.format(usage)) raise OpenSSLObjectError('Unknown key usage "{0}"'.format(usage))
def cryptography_parse_key_usage_params(usages): def cryptography_parse_key_usage_params(usages):
''' """
Given a list of key usage identifier strings, returns the parameters for cryptography's x509.KeyUsage(). Given a list of key usage identifier strings, returns the parameters for cryptography's x509.KeyUsage().
Raises an OpenSSLObjectError if an identifier is unknown. Raises an OpenSSLObjectError if an identifier is unknown.
''' """
params = dict( params = dict(
digital_signature=False, digital_signature=False,
content_commitment=False, content_commitment=False,
@@ -584,40 +676,52 @@ def cryptography_parse_key_usage_params(usages):
def cryptography_get_basic_constraints(constraints): def cryptography_get_basic_constraints(constraints):
''' """
Given a list of constraints, returns a tuple (ca, path_length). Given a list of constraints, returns a tuple (ca, path_length).
Raises an OpenSSLObjectError if a constraint is unknown or cannot be parsed. Raises an OpenSSLObjectError if a constraint is unknown or cannot be parsed.
''' """
ca = False ca = False
path_length = None path_length = None
if constraints: if constraints:
for constraint in constraints: for constraint in constraints:
if constraint.startswith('CA:'): if constraint.startswith("CA:"):
if constraint == 'CA:TRUE': if constraint == "CA:TRUE":
ca = True ca = True
elif constraint == 'CA:FALSE': elif constraint == "CA:FALSE":
ca = False ca = False
else: else:
raise OpenSSLObjectError('Unknown basic constraint value "{0}" for CA'.format(constraint[3:])) raise OpenSSLObjectError(
elif constraint.startswith('pathlen:'): 'Unknown basic constraint value "{0}" for CA'.format(
v = constraint[len('pathlen:'):] constraint[3:]
)
)
elif constraint.startswith("pathlen:"):
v = constraint[len("pathlen:") :]
try: try:
path_length = int(v) path_length = int(v)
except Exception as e: except Exception as e:
raise OpenSSLObjectError('Cannot parse path length constraint "{0}" ({1})'.format(v, e)) raise OpenSSLObjectError(
'Cannot parse path length constraint "{0}" ({1})'.format(v, e)
)
else: else:
raise OpenSSLObjectError('Unknown basic constraint "{0}"'.format(constraint)) raise OpenSSLObjectError(
'Unknown basic constraint "{0}"'.format(constraint)
)
return ca, path_length return ca, path_length
def cryptography_key_needs_digest_for_signing(key): def cryptography_key_needs_digest_for_signing(key):
'''Tests whether the given private key requires a digest algorithm for signing. """Tests whether the given private key requires a digest algorithm for signing.
Ed25519 and Ed448 keys do not; they need None to be passed as the digest algorithm. Ed25519 and Ed448 keys do not; they need None to be passed as the digest algorithm.
''' """
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): if CRYPTOGRAPHY_HAS_ED25519 and isinstance(
key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
):
return False return False
if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): if CRYPTOGRAPHY_HAS_ED448 and isinstance(
key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey
):
return False return False
return True return True
@@ -635,16 +739,22 @@ def _compare_public_keys(key1, key2, clazz):
def cryptography_compare_public_keys(key1, key2): def cryptography_compare_public_keys(key1, key2):
'''Tests whether two public keys are the same. """Tests whether two public keys are the same.
Needs special logic for Ed25519 and Ed448 keys, since they do not have public_numbers(). Needs special logic for Ed25519 and Ed448 keys, since they do not have public_numbers().
''' """
if CRYPTOGRAPHY_HAS_ED25519: if CRYPTOGRAPHY_HAS_ED25519:
res = _compare_public_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey) res = _compare_public_keys(
key1,
key2,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey,
)
if res is not None: if res is not None:
return res return res
if CRYPTOGRAPHY_HAS_ED448: if CRYPTOGRAPHY_HAS_ED448:
res = _compare_public_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey) res = _compare_public_keys(
key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey
)
if res is not None: if res is not None:
return res return res
return key1.public_numbers() == key2.public_numbers() return key1.public_numbers() == key2.public_numbers()
@@ -661,41 +771,61 @@ def _compare_private_keys(key1, key2, clazz, has_no_private_bytes=False):
# We do not have the private_bytes() function - compare associated public keys # We do not have the private_bytes() function - compare associated public keys
return cryptography_compare_public_keys(a.public_key(), b.public_key()) return cryptography_compare_public_keys(a.public_key(), b.public_key())
encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption() encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption()
a = key1.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, encryption_algorithm=encryption_algorithm) a = key1.private_bytes(
b = key2.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, encryption_algorithm=encryption_algorithm) serialization.Encoding.Raw,
serialization.PrivateFormat.Raw,
encryption_algorithm=encryption_algorithm,
)
b = key2.private_bytes(
serialization.Encoding.Raw,
serialization.PrivateFormat.Raw,
encryption_algorithm=encryption_algorithm,
)
return a == b return a == b
def cryptography_compare_private_keys(key1, key2): def cryptography_compare_private_keys(key1, key2):
'''Tests whether two private keys are the same. """Tests whether two private keys are the same.
Needs special logic for Ed25519, X25519, and Ed448 keys, since they do not have private_numbers(). Needs special logic for Ed25519, X25519, and Ed448 keys, since they do not have private_numbers().
''' """
if CRYPTOGRAPHY_HAS_ED25519: if CRYPTOGRAPHY_HAS_ED25519:
res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey) res = _compare_private_keys(
key1,
key2,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey,
)
if res is not None: if res is not None:
return res return res
if CRYPTOGRAPHY_HAS_X25519: if CRYPTOGRAPHY_HAS_X25519:
res = _compare_private_keys( res = _compare_private_keys(
key1, key2, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey, has_no_private_bytes=not CRYPTOGRAPHY_HAS_X25519_FULL) key1,
key2,
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey,
has_no_private_bytes=not CRYPTOGRAPHY_HAS_X25519_FULL,
)
if res is not None: if res is not None:
return res return res
if CRYPTOGRAPHY_HAS_ED448: if CRYPTOGRAPHY_HAS_ED448:
res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey) res = _compare_private_keys(
key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey
)
if res is not None: if res is not None:
return res return res
if CRYPTOGRAPHY_HAS_X448: if CRYPTOGRAPHY_HAS_X448:
res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey) res = _compare_private_keys(
key1, key2, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey
)
if res is not None: if res is not None:
return res return res
return key1.private_numbers() == key2.private_numbers() return key1.private_numbers() == key2.private_numbers()
def cryptography_serial_number_of_cert(cert): def cryptography_serial_number_of_cert(cert):
'''Returns cert.serial_number. """Returns cert.serial_number.
Also works for old versions of cryptography. Also works for old versions of cryptography.
''' """
try: try:
return cert.serial_number return cert.serial_number
except AttributeError: except AttributeError:
@@ -704,10 +834,11 @@ def cryptography_serial_number_of_cert(cert):
def parse_pkcs12(pkcs12_bytes, passphrase=None): def parse_pkcs12(pkcs12_bytes, passphrase=None):
'''Returns a tuple (private_key, certificate, additional_certificates, friendly_name). """Returns a tuple (private_key, certificate, additional_certificates, friendly_name)."""
'''
if _load_pkcs12 is None and _load_key_and_certificates is None: if _load_pkcs12 is None and _load_key_and_certificates is None:
raise ValueError('neither load_pkcs12() nor load_key_and_certificates() present in the current cryptography version') raise ValueError(
"neither load_pkcs12() nor load_key_and_certificates() present in the current cryptography version"
)
if passphrase is not None: if passphrase is not None:
passphrase = to_bytes(passphrase) passphrase = to_bytes(passphrase)
@@ -716,7 +847,7 @@ def parse_pkcs12(pkcs12_bytes, passphrase=None):
if _load_pkcs12 is not None: if _load_pkcs12 is not None:
return _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase) return _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase)
if LooseVersion(cryptography.__version__) >= LooseVersion('35.0'): if LooseVersion(cryptography.__version__) >= LooseVersion("35.0"):
return _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase) return _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase)
return _parse_pkcs12_legacy(pkcs12_bytes, passphrase) return _parse_pkcs12_legacy(pkcs12_bytes, passphrase)
@@ -737,7 +868,9 @@ def _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase=None):
def _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase=None): def _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase=None):
# Backwards compatibility code for cryptography 35.x # Backwards compatibility code for cryptography 35.x
private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase) private_key, certificate, additional_certificates = _load_key_and_certificates(
pkcs12_bytes, passphrase
)
friendly_name = None friendly_name = None
if certificate: if certificate:
@@ -747,18 +880,26 @@ def _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase=None):
# This code basically does what load_key_and_certificates() does, but without error-checking. # This code basically does what load_key_and_certificates() does, but without error-checking.
# Since load_key_and_certificates succeeded, it should not fail. # Since load_key_and_certificates succeeded, it should not fail.
pkcs12 = backend._ffi.gc( pkcs12 = backend._ffi.gc(
backend._lib.d2i_PKCS12_bio(backend._bytes_to_bio(pkcs12_bytes).bio, backend._ffi.NULL), backend._lib.d2i_PKCS12_bio(
backend._lib.PKCS12_free) backend._bytes_to_bio(pkcs12_bytes).bio, backend._ffi.NULL
),
backend._lib.PKCS12_free,
)
certificate_x509_ptr = backend._ffi.new("X509 **") 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: with backend._zeroed_null_terminated_buf(
to_bytes(passphrase) if passphrase is not None else None
) as passphrase_buffer:
backend._lib.PKCS12_parse( backend._lib.PKCS12_parse(
pkcs12, pkcs12,
passphrase_buffer, passphrase_buffer,
backend._ffi.new("EVP_PKEY **"), backend._ffi.new("EVP_PKEY **"),
certificate_x509_ptr, certificate_x509_ptr,
backend._ffi.new("Cryptography_STACK_OF_X509 **")) backend._ffi.new("Cryptography_STACK_OF_X509 **"),
)
if certificate_x509_ptr[0] != backend._ffi.NULL: if certificate_x509_ptr[0] != backend._ffi.NULL:
maybe_name = backend._lib.X509_alias_get0(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: if maybe_name != backend._ffi.NULL:
friendly_name = backend._ffi.string(maybe_name) friendly_name = backend._ffi.string(maybe_name)
@@ -767,7 +908,9 @@ def _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase=None):
def _parse_pkcs12_legacy(pkcs12_bytes, passphrase=None): def _parse_pkcs12_legacy(pkcs12_bytes, passphrase=None):
# Backwards compatibility code for cryptography < 35.0.0 # Backwards compatibility code for cryptography < 35.0.0
private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase) private_key, certificate, additional_certificates = _load_key_and_certificates(
pkcs12_bytes, passphrase
)
friendly_name = None friendly_name = None
if certificate: if certificate:
@@ -780,39 +923,62 @@ def _parse_pkcs12_legacy(pkcs12_bytes, passphrase=None):
def cryptography_verify_signature(signature, data, hash_algorithm, signer_public_key): def cryptography_verify_signature(signature, data, hash_algorithm, signer_public_key):
''' """
Check whether the given signature of the given data was signed by the given public key object. Check whether the given signature of the given data was signed by the given public key object.
''' """
try: try:
if CRYPTOGRAPHY_HAS_RSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey): if CRYPTOGRAPHY_HAS_RSA_SIGN and isinstance(
signer_public_key.verify(signature, data, padding.PKCS1v15(), hash_algorithm) signer_public_key,
cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey,
):
signer_public_key.verify(
signature, data, padding.PKCS1v15(), hash_algorithm
)
return True return True
if CRYPTOGRAPHY_HAS_EC_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey): if CRYPTOGRAPHY_HAS_EC_SIGN and isinstance(
signer_public_key.verify(signature, data, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hash_algorithm)) signer_public_key,
cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey,
):
signer_public_key.verify(
signature,
data,
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hash_algorithm),
)
return True return True
if CRYPTOGRAPHY_HAS_DSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey): if CRYPTOGRAPHY_HAS_DSA_SIGN and isinstance(
signer_public_key,
cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey,
):
signer_public_key.verify(signature, data, hash_algorithm) signer_public_key.verify(signature, data, hash_algorithm)
return True return True
if CRYPTOGRAPHY_HAS_ED25519_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey): if CRYPTOGRAPHY_HAS_ED25519_SIGN and isinstance(
signer_public_key,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey,
):
signer_public_key.verify(signature, data) signer_public_key.verify(signature, data)
return True return True
if CRYPTOGRAPHY_HAS_ED448_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey): if CRYPTOGRAPHY_HAS_ED448_SIGN and isinstance(
signer_public_key,
cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey,
):
signer_public_key.verify(signature, data) signer_public_key.verify(signature, data)
return True return True
raise OpenSSLObjectError(u'Unsupported public key type {0}'.format(type(signer_public_key))) raise OpenSSLObjectError(
u"Unsupported public key type {0}".format(type(signer_public_key))
)
except InvalidSignature: except InvalidSignature:
return False return False
def cryptography_verify_certificate_signature(certificate, signer_public_key): def cryptography_verify_certificate_signature(certificate, signer_public_key):
''' """
Check whether the given X509 certificate object was signed by the given public key object. Check whether the given X509 certificate object was signed by the given public key object.
''' """
return cryptography_verify_signature( return cryptography_verify_signature(
certificate.signature, certificate.signature,
certificate.tbs_certificate_bytes, certificate.tbs_certificate_bytes,
certificate.signature_hash_algorithm, certificate.signature_hash_algorithm,
signer_public_key signer_public_key,
) )

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -12,7 +14,7 @@ import sys
def binary_exp_mod(f, e, m): def binary_exp_mod(f, e, m):
'''Computes f^e mod m in O(log e) multiplications modulo m.''' """Computes f^e mod m in O(log e) multiplications modulo m."""
# Compute len_e = floor(log_2(e)) # Compute len_e = floor(log_2(e))
len_e = -1 len_e = -1
x = e x = e
@@ -29,18 +31,18 @@ def binary_exp_mod(f, e, m):
def simple_gcd(a, b): def simple_gcd(a, b):
'''Compute GCD of its two inputs.''' """Compute GCD of its two inputs."""
while b != 0: while b != 0:
a, b = b, a % b a, b = b, a % b
return a return a
def quick_is_not_prime(n): def quick_is_not_prime(n):
'''Does some quick checks to see if we can poke a hole into the primality of n. """Does some quick checks to see if we can poke a hole into the primality of n.
A result of `False` does **not** mean that the number is prime; it just means A result of `False` does **not** mean that the number is prime; it just means
that we could not detect quickly whether it is not prime. that we could not detect quickly whether it is not prime.
''' """
if n <= 2: if n <= 2:
return n < 2 return n < 2
# The constant in the next line is the product of all primes < 200 # The constant in the next line is the product of all primes < 200
@@ -50,9 +52,52 @@ def quick_is_not_prime(n):
if n < 200 and gcd == n: if n < 200 and gcd == n:
# Explicitly check for all primes < 200 # Explicitly check for all primes < 200
return n not in ( return n not in (
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 2,
89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 3,
181, 191, 193, 197, 199, 5,
7,
11,
13,
17,
19,
23,
29,
31,
37,
41,
43,
47,
53,
59,
61,
67,
71,
73,
79,
83,
89,
97,
101,
103,
107,
109,
113,
127,
131,
137,
139,
149,
151,
157,
163,
167,
173,
179,
181,
191,
193,
197,
199,
) )
return True return True
# TODO: maybe do some iterations of Miller-Rabin to increase confidence # TODO: maybe do some iterations of Miller-Rabin to increase confidence
@@ -81,6 +126,7 @@ if python_version >= (2, 7) or python_version >= (3, 1):
if no == 0: if no == 0:
return 0 return 0
return no.bit_length() return no.bit_length()
else: else:
# Slow, but works # Slow, but works
def count_bytes(no): def count_bytes(no):
@@ -105,25 +151,27 @@ else:
count += 1 count += 1
return count return count
if sys.version_info[0] >= 3: if sys.version_info[0] >= 3:
# Python 3 (and newer) # Python 3 (and newer)
def _convert_int_to_bytes(count, no): def _convert_int_to_bytes(count, no):
return no.to_bytes(count, byteorder='big') return no.to_bytes(count, byteorder="big")
def _convert_bytes_to_int(data): def _convert_bytes_to_int(data):
return int.from_bytes(data, byteorder='big', signed=False) return int.from_bytes(data, byteorder="big", signed=False)
def _to_hex(no): def _to_hex(no):
return hex(no)[2:] return hex(no)[2:]
else: else:
# Python 2 # Python 2
def _convert_int_to_bytes(count, n): def _convert_int_to_bytes(count, n):
if n == 0 and count == 0: if n == 0 and count == 0:
return '' return ""
h = '%x' % n h = "%x" % n
if len(h) > 2 * count: if len(h) > 2 * count:
raise Exception('Number {1} needs more than {0} bytes!'.format(count, n)) raise Exception("Number {1} needs more than {0} bytes!".format(count, n))
return ('0' * (2 * count - len(h)) + h).decode('hex') return ("0" * (2 * count - len(h)) + h).decode("hex")
def _convert_bytes_to_int(data): def _convert_bytes_to_int(data):
v = 0 v = 0
@@ -132,7 +180,7 @@ else:
return v return v
def _to_hex(no): def _to_hex(no):
return '%x' % no return "%x" % no
def convert_int_to_bytes(no, count=None): def convert_int_to_bytes(no, count=None):
@@ -162,7 +210,7 @@ def convert_int_to_hex(no, digits=None):
no = abs(no) no = abs(no)
value = _to_hex(no) value = _to_hex(no)
if digits is not None and len(value) < digits: if digits is not None and len(value) < digits:
value = '0' * (digits - len(value)) + value value = "0" * (digits - len(value)) + value
return value return value

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -14,39 +16,39 @@ import traceback
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_collections.community.crypto.plugins.module_utils.argspec import (
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec ArgumentSpec,
)
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
OpenSSLBadPassphraseError, OpenSSLBadPassphraseError,
OpenSSLObjectError,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_privatekey,
load_certificate,
load_certificate_request,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
cryptography_compare_public_keys, cryptography_compare_public_keys,
get_not_valid_after, get_not_valid_after,
get_not_valid_before, get_not_valid_before,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
get_certificate_info, get_certificate_info,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_certificate,
load_certificate_request,
load_privatekey,
)
from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
MINIMAL_CRYPTOGRAPHY_VERSION = "1.6"
CRYPTOGRAPHY_IMP_ERR = None CRYPTOGRAPHY_IMP_ERR = None
CRYPTOGRAPHY_VERSION = None CRYPTOGRAPHY_VERSION = None
try: try:
import cryptography import cryptography
from cryptography import x509 from cryptography import x509
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError: except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -65,21 +67,21 @@ class CertificateBackend(object):
self.module = module self.module = module
self.backend = backend self.backend = backend
self.force = module.params['force'] self.force = module.params["force"]
self.ignore_timestamps = module.params['ignore_timestamps'] self.ignore_timestamps = module.params["ignore_timestamps"]
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"]
if self.privatekey_content is not None: if self.privatekey_content is not None:
self.privatekey_content = self.privatekey_content.encode('utf-8') self.privatekey_content = self.privatekey_content.encode("utf-8")
self.privatekey_passphrase = module.params['privatekey_passphrase'] self.privatekey_passphrase = module.params["privatekey_passphrase"]
self.csr_path = module.params['csr_path'] self.csr_path = module.params["csr_path"]
self.csr_content = module.params['csr_content'] self.csr_content = module.params["csr_content"]
if self.csr_content is not None: if self.csr_content is not None:
self.csr_content = self.csr_content.encode('utf-8') self.csr_content = self.csr_content.encode("utf-8")
# The following are default values which make sure check() works as # The following are default values which make sure check() works as
# before if providers do not explicitly change these properties. # before if providers do not explicitly change these properties.
self.create_subject_key_identifier = 'never_create' self.create_subject_key_identifier = "never_create"
self.create_authority_key_identifier = False self.create_authority_key_identifier = False
self.privatekey = None self.privatekey = None
@@ -98,10 +100,12 @@ class CertificateBackend(object):
if data is None: if data is None:
return dict() return dict()
try: try:
result = get_certificate_info(self.module, self.backend, data, prefer_one_fingerprint=True) result = get_certificate_info(
result['can_parse_certificate'] = True self.module, self.backend, data, prefer_one_fingerprint=True
)
result["can_parse_certificate"] = True
return result return result
except Exception as exc: except Exception:
return dict(can_parse_certificate=False) return dict(can_parse_certificate=False)
@abc.abstractmethod @abc.abstractmethod
@@ -117,7 +121,9 @@ 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) 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."""
@@ -165,33 +171,60 @@ class CertificateBackend(object):
def _check_privatekey(self): def _check_privatekey(self):
"""Check whether provided parameters match, assuming self.existing_certificate and self.privatekey have been populated.""" """Check whether provided parameters match, assuming self.existing_certificate and self.privatekey have been populated."""
if self.backend == 'cryptography': if self.backend == "cryptography":
return cryptography_compare_public_keys(self.existing_certificate.public_key(), self.privatekey.public_key()) return cryptography_compare_public_keys(
self.existing_certificate.public_key(), self.privatekey.public_key()
)
def _check_csr(self): def _check_csr(self):
"""Check whether provided parameters match, assuming self.existing_certificate and self.csr have been populated.""" """Check whether provided parameters match, assuming self.existing_certificate and self.csr have been populated."""
if self.backend == 'cryptography': if self.backend == "cryptography":
# Verify that CSR is signed by certificate's private key # Verify that CSR is signed by certificate's private key
if not self.csr.is_signature_valid: if not self.csr.is_signature_valid:
return False return False
if not cryptography_compare_public_keys(self.csr.public_key(), self.existing_certificate.public_key()): if not cryptography_compare_public_keys(
self.csr.public_key(), self.existing_certificate.public_key()
):
return False return False
# Check subject # Check subject
if self.check_csr_subject and self.csr.subject != self.existing_certificate.subject: if (
self.check_csr_subject
and self.csr.subject != self.existing_certificate.subject
):
return False return False
# Check extensions # Check extensions
if not self.check_csr_extensions: if not self.check_csr_extensions:
return True return True
cert_exts = list(self.existing_certificate.extensions) cert_exts = list(self.existing_certificate.extensions)
csr_exts = list(self.csr.extensions) csr_exts = list(self.csr.extensions)
if self.create_subject_key_identifier != 'never_create': if self.create_subject_key_identifier != "never_create":
# Filter out SubjectKeyIdentifier extension before comparison # Filter out SubjectKeyIdentifier extension before comparison
cert_exts = list(filter(lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier), cert_exts)) cert_exts = list(
csr_exts = list(filter(lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier), csr_exts)) filter(
lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier),
cert_exts,
)
)
csr_exts = list(
filter(
lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier),
csr_exts,
)
)
if self.create_authority_key_identifier: if self.create_authority_key_identifier:
# Filter out AuthorityKeyIdentifier extension before comparison # Filter out AuthorityKeyIdentifier extension before comparison
cert_exts = list(filter(lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier), cert_exts)) cert_exts = list(
csr_exts = list(filter(lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier), csr_exts)) filter(
lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier),
cert_exts,
)
)
csr_exts = list(
filter(
lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier),
csr_exts,
)
)
if len(cert_exts) != len(csr_exts): if len(cert_exts) != len(csr_exts):
return False return False
for cert_ext in cert_exts: for cert_ext in cert_exts:
@@ -199,7 +232,7 @@ class CertificateBackend(object):
csr_ext = self.csr.extensions.get_extension_for_oid(cert_ext.oid) csr_ext = self.csr.extensions.get_extension_for_oid(cert_ext.oid)
if cert_ext != csr_ext: if cert_ext != csr_ext:
return False return False
except cryptography.x509.ExtensionNotFound as dummy: except cryptography.x509.ExtensionNotFound:
return False return False
return True return True
@@ -207,19 +240,28 @@ class CertificateBackend(object):
"""Check whether Subject Key Identifier matches, assuming self.existing_certificate has been populated.""" """Check whether Subject Key Identifier matches, assuming self.existing_certificate has been populated."""
# Get hold of certificate's SKI # Get hold of certificate's SKI
try: try:
ext = self.existing_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) ext = self.existing_certificate.extensions.get_extension_for_class(
except cryptography.x509.ExtensionNotFound as dummy: x509.SubjectKeyIdentifier
)
except cryptography.x509.ExtensionNotFound:
return False return False
# Get hold of CSR's SKI for 'create_if_not_provided' # Get hold of CSR's SKI for 'create_if_not_provided'
csr_ext = None csr_ext = None
if self.create_subject_key_identifier == 'create_if_not_provided': if self.create_subject_key_identifier == "create_if_not_provided":
try: try:
csr_ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) csr_ext = self.csr.extensions.get_extension_for_class(
except cryptography.x509.ExtensionNotFound as dummy: x509.SubjectKeyIdentifier
)
except cryptography.x509.ExtensionNotFound:
pass pass
if csr_ext is None: if csr_ext is None:
# If CSR had no SKI, or we chose to ignore it ('always_create'), compare with created SKI # If CSR had no SKI, or we chose to ignore it ('always_create'), compare with created SKI
if ext.value.digest != x509.SubjectKeyIdentifier.from_public_key(self.existing_certificate.public_key()).digest: if (
ext.value.digest
!= x509.SubjectKeyIdentifier.from_public_key(
self.existing_certificate.public_key()
).digest
):
return False return False
else: else:
# If CSR had SKI and we did not ignore it ('create_if_not_provided'), compare SKIs # If CSR had SKI and we did not ignore it ('create_if_not_provided'), compare SKIs
@@ -234,7 +276,7 @@ class CertificateBackend(object):
try: try:
self._ensure_existing_certificate_loaded() self._ensure_existing_certificate_loaded()
except Exception as dummy: except Exception:
return True return True
# Check whether private key matches # Check whether private key matches
@@ -248,7 +290,10 @@ class CertificateBackend(object):
return True return True
# Check SubjectKeyIdentifier # Check SubjectKeyIdentifier
if self.create_subject_key_identifier != 'never_create' and not self._check_subject_key_identifier(): if (
self.create_subject_key_identifier != "never_create"
and not self._check_subject_key_identifier()
):
return True return True
# Check not before # Check not before
@@ -264,10 +309,7 @@ class CertificateBackend(object):
def dump(self, include_certificate): def dump(self, include_certificate):
"""Serialize the object into a dictionary.""" """Serialize the object into a dictionary."""
result = { result = {"privatekey": self.privatekey_path, "csr": self.csr_path}
'privatekey': self.privatekey_path,
'csr': self.csr_path
}
# Get hold of certificate bytes # Get hold of certificate bytes
certificate_bytes = self.existing_certificate_bytes certificate_bytes = self.existing_certificate_bytes
if self.cert is not None: if self.cert is not None:
@@ -275,9 +317,11 @@ class CertificateBackend(object):
self.diff_after = self._get_info(certificate_bytes) self.diff_after = self._get_info(certificate_bytes)
if include_certificate: if include_certificate:
# 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( result["diff"] = dict(
before=self.diff_before, before=self.diff_before,
after=self.diff_after, after=self.diff_after,
) )
@@ -310,26 +354,38 @@ def select_backend(module, backend, provider):
""" """
provider.validate_module_args(module) provider.validate_module_args(module)
backend = module.params['select_crypto_backend'] backend = module.params["select_crypto_backend"]
if backend == 'auto': if backend == "auto":
# Detect what backend we can use # Detect what backend we can use
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) can_use_cryptography = (
CRYPTOGRAPHY_FOUND
and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
)
# If cryptography is available we'll use it # If cryptography is available we'll use it
if can_use_cryptography: if can_use_cryptography:
backend = 'cryptography' backend = "cryptography"
# Fail if no backend has been found # Fail if no backend has been found
if backend == 'auto': if backend == "auto":
module.fail_json(msg=("Cannot detect the required Python library " module.fail_json(
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) msg=(
"Cannot detect the required Python library " "cryptography (>= {0})"
).format(MINIMAL_CRYPTOGRAPHY_VERSION)
)
if backend == 'cryptography': if backend == "cryptography":
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), module.fail_json(
exception=CRYPTOGRAPHY_IMP_ERR) msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
if provider.needs_version_two_certs(module): if provider.needs_version_two_certs(module):
module.fail_json(msg='The cryptography backend does not support v2 certificates') module.fail_json(
msg="The cryptography backend does not support v2 certificates"
)
return provider.create_backend(module, backend) return provider.create_backend(module, backend)
@@ -337,20 +393,26 @@ def select_backend(module, backend, provider):
def get_certificate_argument_spec(): def get_certificate_argument_spec():
return ArgumentSpec( return ArgumentSpec(
argument_spec=dict( argument_spec=dict(
provider=dict(type='str', choices=[]), # choices will be filled by add_XXX_provider_to_argument_spec() in certificate_xxx.py provider=dict(
force=dict(type='bool', default=False,), type="str", choices=[]
csr_path=dict(type='path'), ), # choices will be filled by add_XXX_provider_to_argument_spec() in certificate_xxx.py
csr_content=dict(type='str'), force=dict(
ignore_timestamps=dict(type='bool', default=True), type="bool",
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']), default=False,
),
csr_path=dict(type="path"),
csr_content=dict(type="str"),
ignore_timestamps=dict(type="bool", default=True),
select_crypto_backend=dict(
type="str", default="auto", choices=["auto", "cryptography"]
),
# General properties of a certificate # General properties of a certificate
privatekey_path=dict(type='path'), privatekey_path=dict(type="path"),
privatekey_content=dict(type='str', no_log=True), privatekey_content=dict(type="str", no_log=True),
privatekey_passphrase=dict(type='str', no_log=True), privatekey_passphrase=dict(type="str", no_log=True),
), ),
mutually_exclusive=[ mutually_exclusive=[
['csr_path', 'csr_content'], ["csr_path", "csr_content"],
['privatekey_path', 'privatekey_content'], ["privatekey_path", "privatekey_content"],
], ],
) )

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -13,11 +15,10 @@ import os
import tempfile import tempfile
import traceback import traceback
from ansible.module_utils.common.text.converters import to_native, to_bytes from ansible.module_utils.common.text.converters import to_bytes, 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 (
CertificateError,
CertificateBackend, CertificateBackend,
CertificateError,
CertificateProvider, CertificateProvider,
) )
@@ -25,61 +26,61 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
class AcmeCertificateBackend(CertificateBackend): class AcmeCertificateBackend(CertificateBackend):
def __init__(self, module, backend): def __init__(self, module, backend):
super(AcmeCertificateBackend, self).__init__(module, backend) super(AcmeCertificateBackend, self).__init__(module, backend)
self.accountkey_path = module.params['acme_accountkey_path'] self.accountkey_path = module.params["acme_accountkey_path"]
self.challenge_path = module.params['acme_challenge_path'] self.challenge_path = module.params["acme_challenge_path"]
self.use_chain = module.params['acme_chain'] self.use_chain = module.params["acme_chain"]
self.acme_directory = module.params['acme_directory'] self.acme_directory = module.params["acme_directory"]
if self.csr_content is None and self.csr_path is None: if self.csr_content is None and self.csr_path is None:
raise CertificateError( raise CertificateError(
'csr_path or csr_content is required for ownca provider' "csr_path or csr_content is required for ownca provider"
) )
if self.csr_content is None and not os.path.exists(self.csr_path): if self.csr_content is None and not os.path.exists(self.csr_path):
raise CertificateError( raise CertificateError(
'The certificate signing request file %s does not exist' % self.csr_path "The certificate signing request file %s does not exist" % self.csr_path
) )
if not os.path.exists(self.accountkey_path): if not os.path.exists(self.accountkey_path):
raise CertificateError( raise CertificateError(
'The account key %s does not exist' % self.accountkey_path "The account key %s does not exist" % self.accountkey_path
) )
if not os.path.exists(self.challenge_path): if not os.path.exists(self.challenge_path):
raise CertificateError( raise CertificateError(
'The challenge path %s does not exist' % self.challenge_path "The challenge path %s does not exist" % self.challenge_path
) )
self.acme_tiny_path = self.module.get_bin_path('acme-tiny', required=True) self.acme_tiny_path = self.module.get_bin_path("acme-tiny", required=True)
def generate_certificate(self): def generate_certificate(self):
"""(Re-)Generate certificate.""" """(Re-)Generate certificate."""
command = [self.acme_tiny_path] command = [self.acme_tiny_path]
if self.use_chain: if self.use_chain:
command.append('--chain') command.append("--chain")
command.extend(['--account-key', self.accountkey_path]) command.extend(["--account-key", self.accountkey_path])
if self.csr_content is not None: if self.csr_content is not None:
# We need to temporarily write the CSR to disk # We need to temporarily write the CSR to disk
fd, tmpsrc = tempfile.mkstemp() fd, tmpsrc = tempfile.mkstemp()
self.module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit self.module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
f = os.fdopen(fd, 'wb') f = os.fdopen(fd, "wb")
try: try:
f.write(self.csr_content) f.write(self.csr_content)
except Exception as err: except Exception as err:
try: try:
f.close() f.close()
except Exception as dummy: except Exception:
pass pass
self.module.fail_json( self.module.fail_json(
msg="failed to create temporary CSR file: %s" % to_native(err), msg="failed to create temporary CSR file: %s" % to_native(err),
exception=traceback.format_exc() exception=traceback.format_exc(),
) )
f.close() f.close()
command.extend(['--csr', tmpsrc]) command.extend(["--csr", tmpsrc])
else: else:
command.extend(['--csr', self.csr_path]) command.extend(["--csr", self.csr_path])
command.extend(['--acme-dir', self.challenge_path]) command.extend(["--acme-dir", self.challenge_path])
command.extend(['--directory-url', self.acme_directory]) command.extend(["--directory-url", self.acme_directory])
try: try:
self.cert = to_bytes(self.module.run_command(command, check_rc=True)[1]) self.cert = to_bytes(self.module.run_command(command, check_rc=True)[1])
@@ -92,16 +93,20 @@ class AcmeCertificateBackend(CertificateBackend):
def dump(self, include_certificate): def dump(self, include_certificate):
result = super(AcmeCertificateBackend, self).dump(include_certificate) result = super(AcmeCertificateBackend, self).dump(include_certificate)
result['accountkey'] = self.accountkey_path result["accountkey"] = self.accountkey_path
return result return result
class AcmeCertificateProvider(CertificateProvider): class AcmeCertificateProvider(CertificateProvider):
def validate_module_args(self, module): def validate_module_args(self, module):
if module.params['acme_accountkey_path'] is None: if module.params["acme_accountkey_path"] is None:
module.fail_json(msg='The acme_accountkey_path option must be specified for the acme provider.') module.fail_json(
if module.params['acme_challenge_path'] is None: msg="The acme_accountkey_path option must be specified for the acme provider."
module.fail_json(msg='The acme_challenge_path option must be specified for the acme provider.') )
if module.params["acme_challenge_path"] is None:
module.fail_json(
msg="The acme_challenge_path option must be specified for the acme provider."
)
def needs_version_two_certs(self, module): def needs_version_two_certs(self, module):
return False return False
@@ -111,10 +116,14 @@ class AcmeCertificateProvider(CertificateProvider):
def add_acme_provider_to_argument_spec(argument_spec): def add_acme_provider_to_argument_spec(argument_spec):
argument_spec.argument_spec['provider']['choices'].append('acme') argument_spec.argument_spec["provider"]["choices"].append("acme")
argument_spec.argument_spec.update(dict( argument_spec.argument_spec.update(
acme_accountkey_path=dict(type='path'), dict(
acme_challenge_path=dict(type='path'), acme_accountkey_path=dict(type="path"),
acme_chain=dict(type='bool', default=False), acme_challenge_path=dict(type="path"),
acme_directory=dict(type='str', default="https://acme-v02.api.letsencrypt.org/directory"), acme_chain=dict(type="bool", default=False),
)) acme_directory=dict(
type="str", default="https://acme-v02.api.letsencrypt.org/directory"
),
)
)

View File

@@ -6,37 +6,39 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import datetime import datetime
import os import os
from ansible.module_utils.common.text.converters import to_native, to_bytes from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible_collections.community.crypto.plugins.module_utils.ecs.api import ECSClient, RestOperationException, SessionConfigurationException
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_certificate,
)
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_TIMEZONE, CRYPTOGRAPHY_TIMEZONE,
cryptography_serial_number_of_cert, cryptography_serial_number_of_cert,
get_not_valid_after, get_not_valid_after,
) )
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,
CertificateBackend, CertificateBackend,
CertificateError,
CertificateProvider, CertificateProvider,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_certificate,
)
from ansible_collections.community.crypto.plugins.module_utils.ecs.api import (
ECSClient,
RestOperationException,
SessionConfigurationException,
)
from ansible_collections.community.crypto.plugins.module_utils.time import ( from ansible_collections.community.crypto.plugins.module_utils.time import (
get_now_datetime, get_now_datetime,
get_relative_time_option, get_relative_time_option,
) )
try: try:
from cryptography.x509.oid import NameOID from cryptography.x509.oid import NameOID
except ImportError: except ImportError:
@@ -48,19 +50,21 @@ class EntrustCertificateBackend(CertificateBackend):
super(EntrustCertificateBackend, self).__init__(module, backend) super(EntrustCertificateBackend, self).__init__(module, backend)
self.trackingId = None self.trackingId = None
self.notAfter = get_relative_time_option( self.notAfter = get_relative_time_option(
module.params['entrust_not_after'], module.params["entrust_not_after"],
'entrust_not_after', "entrust_not_after",
backend=self.backend, backend=self.backend,
with_timezone=CRYPTOGRAPHY_TIMEZONE, with_timezone=CRYPTOGRAPHY_TIMEZONE,
) )
if self.csr_content is None and self.csr_path is None: if self.csr_content is None and self.csr_path is None:
raise CertificateError( raise CertificateError(
'csr_path or csr_content is required for entrust provider' "csr_path or csr_content is required for entrust provider"
) )
if self.csr_content is None and not os.path.exists(self.csr_path): if self.csr_content is None and not os.path.exists(self.csr_path):
raise CertificateError( raise CertificateError(
'The certificate signing request file {0} does not exist'.format(self.csr_path) "The certificate signing request file {0} does not exist".format(
self.csr_path
)
) )
self._ensure_csr_loaded() self._ensure_csr_loaded()
@@ -69,28 +73,42 @@ class EntrustCertificateBackend(CertificateBackend):
# We want to always force behavior of trying to use the organization provided in the CSR. # We want to always force behavior of trying to use the organization provided in the CSR.
# To that end we need to parse out the organization from the CSR. # To that end we need to parse out the organization from the CSR.
self.csr_org = None self.csr_org = None
if self.backend == 'cryptography': if self.backend == "cryptography":
csr_subject_orgs = self.csr.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME) csr_subject_orgs = self.csr.subject.get_attributes_for_oid(
NameOID.ORGANIZATION_NAME
)
if len(csr_subject_orgs) == 1: if len(csr_subject_orgs) == 1:
self.csr_org = csr_subject_orgs[0].value self.csr_org = csr_subject_orgs[0].value
elif len(csr_subject_orgs) > 1: elif len(csr_subject_orgs) > 1:
self.module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations found in " self.module.fail_json(
"Subject DN: '{0}'. ".format(self.csr.subject))) msg=(
"Entrust provider does not currently support multiple validated organizations. Multiple organizations found in "
"Subject DN: '{0}'. ".format(self.csr.subject)
)
)
# If no organization in the CSR, explicitly tell ECS that it should be blank in issued cert, not defaulted to # If no organization in the CSR, explicitly tell ECS that it should be blank in issued cert, not defaulted to
# organization tied to the account. # organization tied to the account.
if self.csr_org is None: if self.csr_org is None:
self.csr_org = '' self.csr_org = ""
try: try:
self.ecs_client = ECSClient( self.ecs_client = ECSClient(
entrust_api_user=self.module.params['entrust_api_user'], entrust_api_user=self.module.params["entrust_api_user"],
entrust_api_key=self.module.params['entrust_api_key'], entrust_api_key=self.module.params["entrust_api_key"],
entrust_api_cert=self.module.params['entrust_api_client_cert_path'], entrust_api_cert=self.module.params["entrust_api_client_cert_path"],
entrust_api_cert_key=self.module.params['entrust_api_client_cert_key_path'], entrust_api_cert_key=self.module.params[
entrust_api_specification_path=self.module.params['entrust_api_specification_path'] "entrust_api_client_cert_key_path"
],
entrust_api_specification_path=self.module.params[
"entrust_api_specification_path"
],
) )
except SessionConfigurationException as e: except SessionConfigurationException as e:
module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e.message))) module.fail_json(
msg="Failed to initialize Entrust Provider: {0}".format(
to_native(e.message)
)
)
def generate_certificate(self): def generate_certificate(self):
"""(Re-)Generate certificate.""" """(Re-)Generate certificate."""
@@ -99,12 +117,12 @@ class EntrustCertificateBackend(CertificateBackend):
# Read the CSR that was generated for us # Read the CSR that was generated for us
if self.csr_content is not None: if self.csr_content is not None:
# csr_content contains bytes # csr_content contains bytes
body['csr'] = to_native(self.csr_content) body["csr"] = to_native(self.csr_content)
else: else:
with open(self.csr_path, 'r') as csr_file: with open(self.csr_path, "r") as csr_file:
body['csr'] = csr_file.read() body["csr"] = csr_file.read()
body['certType'] = self.module.params['entrust_cert_type'] body["certType"] = self.module.params["entrust_cert_type"]
# Handle expiration (30 days if not specified) # Handle expiration (30 days if not specified)
expiry = self.notAfter expiry = self.notAfter
@@ -113,22 +131,28 @@ class EntrustCertificateBackend(CertificateBackend):
expiry = gmt_now + datetime.timedelta(days=365) expiry = gmt_now + datetime.timedelta(days=365)
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z") expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
body['certExpiryDate'] = expiry_iso3339 body["certExpiryDate"] = expiry_iso3339
body['org'] = self.csr_org body["org"] = self.csr_org
body['tracking'] = { body["tracking"] = {
'requesterName': self.module.params['entrust_requester_name'], "requesterName": self.module.params["entrust_requester_name"],
'requesterEmail': self.module.params['entrust_requester_email'], "requesterEmail": self.module.params["entrust_requester_email"],
'requesterPhone': self.module.params['entrust_requester_phone'], "requesterPhone": self.module.params["entrust_requester_phone"],
} }
try: try:
result = self.ecs_client.NewCertRequest(Body=body) result = self.ecs_client.NewCertRequest(Body=body)
self.trackingId = result.get('trackingId') self.trackingId = result.get("trackingId")
except RestOperationException as e: except RestOperationException as e:
self.module.fail_json(msg='Failed to request new certificate from Entrust Certificate Services (ECS): {0}'.format(to_native(e.message))) self.module.fail_json(
msg="Failed to request new certificate from Entrust Certificate Services (ECS): {0}".format(
to_native(e.message)
)
)
self.cert_bytes = to_bytes(result.get('endEntityCert')) self.cert_bytes = to_bytes(result.get("endEntityCert"))
self.cert = load_certificate(path=None, content=self.cert_bytes, backend=self.backend) self.cert = load_certificate(
path=None, content=self.cert_bytes, backend=self.backend
)
def get_certificate_data(self): def get_certificate_data(self):
"""Return bytes for self.cert.""" """Return bytes for self.cert."""
@@ -140,15 +164,23 @@ class EntrustCertificateBackend(CertificateBackend):
try: try:
cert_details = self._get_cert_details() cert_details = self._get_cert_details()
except RestOperationException as e: except RestOperationException as e:
self.module.fail_json(msg='Failed to get status of existing certificate from Entrust Certificate Services (ECS): {0}.'.format(to_native(e.message))) self.module.fail_json(
msg="Failed to get status of existing certificate from Entrust Certificate Services (ECS): {0}.".format(
to_native(e.message)
)
)
# Always issue a new certificate if the certificate is expired, suspended or revoked # Always issue a new certificate if the certificate is expired, suspended or revoked
status = cert_details.get('status', False) status = cert_details.get("status", False)
if status == 'EXPIRED' or status == 'SUSPENDED' or status == 'REVOKED': if status == "EXPIRED" or status == "SUSPENDED" or status == "REVOKED":
return True return True
# If the requested cert type was specified and it is for a different certificate type than the initial certificate, a new one is needed # If the requested cert type was specified and it is for a different certificate type than the initial certificate, a new one is needed
if self.module.params['entrust_cert_type'] and cert_details.get('certType') and self.module.params['entrust_cert_type'] != cert_details.get('certType'): if (
self.module.params["entrust_cert_type"]
and cert_details.get("certType")
and self.module.params["entrust_cert_type"] != cert_details.get("certType")
):
return True return True
return parent_check return parent_check
@@ -157,32 +189,38 @@ class EntrustCertificateBackend(CertificateBackend):
cert_details = {} cert_details = {}
try: try:
self._ensure_existing_certificate_loaded() self._ensure_existing_certificate_loaded()
except Exception as dummy: except Exception:
return return
if self.existing_certificate: if self.existing_certificate:
serial_number = None serial_number = None
expiry = None expiry = None
if self.backend == 'cryptography': if self.backend == "cryptography":
serial_number = "{0:X}".format(cryptography_serial_number_of_cert(self.existing_certificate)) serial_number = "{0:X}".format(
cryptography_serial_number_of_cert(self.existing_certificate)
)
expiry = get_not_valid_after(self.existing_certificate) expiry = get_not_valid_after(self.existing_certificate)
# get some information about the expiry of this certificate # get some information about the expiry of this certificate
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z") expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
cert_details['expiresAfter'] = expiry_iso3339 cert_details["expiresAfter"] = expiry_iso3339
# If a trackingId is not already defined (from the result of a generate) # If a trackingId is not already defined (from the result of a generate)
# use the serial number to identify the tracking Id # use the serial number to identify the tracking Id
if self.trackingId is None and serial_number is not None: if self.trackingId is None and serial_number is not None:
cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {}) cert_results = self.ecs_client.GetCertificates(
serialNumber=serial_number
).get("certificates", {})
# Finding 0 or more than 1 result is a very unlikely use case, it simply means we cannot perform additional checks # Finding 0 or more than 1 result is a very unlikely use case, it simply means we cannot perform additional checks
# on the 'state' as returned by Entrust Certificate Services (ECS). The general certificate validity is # on the 'state' as returned by Entrust Certificate Services (ECS). The general certificate validity is
# still checked as it is in the rest of the module. # still checked as it is in the rest of the module.
if len(cert_results) == 1: if len(cert_results) == 1:
self.trackingId = cert_results[0].get('trackingId') self.trackingId = cert_results[0].get("trackingId")
if self.trackingId is not None: if self.trackingId is not None:
cert_details.update(self.ecs_client.GetCertificate(trackingId=self.trackingId)) cert_details.update(
self.ecs_client.GetCertificate(trackingId=self.trackingId)
)
return cert_details return cert_details
@@ -199,23 +237,51 @@ class EntrustCertificateProvider(CertificateProvider):
def add_entrust_provider_to_argument_spec(argument_spec): def add_entrust_provider_to_argument_spec(argument_spec):
argument_spec.argument_spec['provider']['choices'].append('entrust') argument_spec.argument_spec["provider"]["choices"].append("entrust")
argument_spec.argument_spec.update(dict( argument_spec.argument_spec.update(
entrust_cert_type=dict(type='str', default='STANDARD_SSL', dict(
choices=['STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', entrust_cert_type=dict(
'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT']), type="str",
entrust_requester_email=dict(type='str'), default="STANDARD_SSL",
entrust_requester_name=dict(type='str'), choices=[
entrust_requester_phone=dict(type='str'), "STANDARD_SSL",
entrust_api_user=dict(type='str'), "ADVANTAGE_SSL",
entrust_api_key=dict(type='str', no_log=True), "UC_SSL",
entrust_api_client_cert_path=dict(type='path'), "EV_SSL",
entrust_api_client_cert_key_path=dict(type='path', no_log=True), "WILDCARD_SSL",
entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'), "PRIVATE_SSL",
entrust_not_after=dict(type='str', default='+365d'), "PD_SSL",
)) "CDS_ENT_LITE",
argument_spec.required_if.append( "CDS_ENT_PRO",
['provider', 'entrust', ['entrust_requester_email', 'entrust_requester_name', 'entrust_requester_phone', "SMIME_ENT",
'entrust_api_user', 'entrust_api_key', 'entrust_api_client_cert_path', ],
'entrust_api_client_cert_key_path']] ),
entrust_requester_email=dict(type="str"),
entrust_requester_name=dict(type="str"),
entrust_requester_phone=dict(type="str"),
entrust_api_user=dict(type="str"),
entrust_api_key=dict(type="str", no_log=True),
entrust_api_client_cert_path=dict(type="path"),
entrust_api_client_cert_key_path=dict(type="path", no_log=True),
entrust_api_specification_path=dict(
type="path",
default="https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml",
),
entrust_not_after=dict(type="str", default="+365d"),
)
)
argument_spec.required_if.append(
[
"provider",
"entrust",
[
"entrust_requester_email",
"entrust_requester_name",
"entrust_requester_phone",
"entrust_api_user",
"entrust_api_key",
"entrust_api_client_cert_path",
"entrust_api_client_cert_key_path",
],
]
) )

View File

@@ -7,6 +7,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -17,14 +19,6 @@ import traceback
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.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
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.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE, CRYPTOGRAPHY_TIMEZONE,
cryptography_decode_name, cryptography_decode_name,
@@ -34,22 +28,29 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
get_not_valid_after, get_not_valid_after,
get_not_valid_before, get_not_valid_before,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
get_publickey_info, get_publickey_info,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
get_fingerprint_of_bytes,
load_certificate,
)
from ansible_collections.community.crypto.plugins.module_utils.time import ( from ansible_collections.community.crypto.plugins.module_utils.time import (
get_now_datetime, get_now_datetime,
) )
from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
MINIMAL_CRYPTOGRAPHY_VERSION = "1.6"
CRYPTOGRAPHY_IMP_ERR = None CRYPTOGRAPHY_IMP_ERR = None
try: try:
import cryptography import cryptography
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError: except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -151,75 +152,97 @@ class CertificateInfoRetrieval(object):
def get_info(self, prefer_one_fingerprint=False, der_support_enabled=False): def get_info(self, prefer_one_fingerprint=False, der_support_enabled=False):
result = dict() result = dict()
self.cert = load_certificate(None, content=self.content, backend=self.backend, der_support_enabled=der_support_enabled) self.cert = load_certificate(
None,
content=self.content,
backend=self.backend,
der_support_enabled=der_support_enabled,
)
result['signature_algorithm'] = self._get_signature_algorithm() result["signature_algorithm"] = self._get_signature_algorithm()
subject = self._get_subject_ordered() subject = self._get_subject_ordered()
issuer = self._get_issuer_ordered() issuer = self._get_issuer_ordered()
result['subject'] = dict() result["subject"] = dict()
for k, v in subject: for k, v in subject:
result['subject'][k] = v result["subject"][k] = v
result['subject_ordered'] = subject result["subject_ordered"] = subject
result['issuer'] = dict() result["issuer"] = dict()
for k, v in issuer: for k, v in issuer:
result['issuer'][k] = v result["issuer"][k] = v
result['issuer_ordered'] = issuer result["issuer_ordered"] = issuer
result['version'] = self._get_version() result["version"] = self._get_version()
result['key_usage'], result['key_usage_critical'] = self._get_key_usage() 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["extended_key_usage"], result["extended_key_usage_critical"] = (
result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints() self._get_extended_key_usage()
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["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_before = self.get_not_before()
not_after = self.get_not_after() not_after = self.get_not_after()
result['not_before'] = not_before.strftime(TIMESTAMP_FORMAT) result["not_before"] = not_before.strftime(TIMESTAMP_FORMAT)
result['not_after'] = not_after.strftime(TIMESTAMP_FORMAT) result["not_after"] = not_after.strftime(TIMESTAMP_FORMAT)
result['expired'] = not_after < get_now_datetime(with_timezone=CRYPTOGRAPHY_TIMEZONE) result["expired"] = not_after < get_now_datetime(
with_timezone=CRYPTOGRAPHY_TIMEZONE
)
result['public_key'] = to_native(self._get_public_key_pem()) result["public_key"] = to_native(self._get_public_key_pem())
public_key_info = get_publickey_info( public_key_info = get_publickey_info(
self.module, self.module,
self.backend, self.backend,
key=self._get_public_key_object(), key=self._get_public_key_object(),
prefer_one_fingerprint=prefer_one_fingerprint) prefer_one_fingerprint=prefer_one_fingerprint,
result.update({ )
'public_key_type': public_key_info['type'], result.update(
'public_key_data': public_key_info['public_data'], {
'public_key_fingerprints': public_key_info['fingerprints'], "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( result["fingerprints"] = get_fingerprint_of_bytes(
self._get_der_bytes(), prefer_one=prefer_one_fingerprint) self._get_der_bytes(), prefer_one=prefer_one_fingerprint
)
ski = self._get_subject_key_identifier() ski = self._get_subject_key_identifier()
if ski is not None: if ski is not None:
ski = to_native(binascii.hexlify(ski)) ski = to_native(binascii.hexlify(ski))
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)]) ski = ":".join([ski[i : i + 2] for i in range(0, len(ski), 2)])
result['subject_key_identifier'] = ski result["subject_key_identifier"] = ski
aki, aci, acsn = self._get_authority_key_identifier() aki, aci, acsn = self._get_authority_key_identifier()
if aki is not None: if aki is not None:
aki = to_native(binascii.hexlify(aki)) aki = to_native(binascii.hexlify(aki))
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)]) aki = ":".join([aki[i : i + 2] for i in range(0, len(aki), 2)])
result['authority_key_identifier'] = aki result["authority_key_identifier"] = aki
result['authority_cert_issuer'] = aci result["authority_cert_issuer"] = aci
result['authority_cert_serial_number'] = acsn result["authority_cert_serial_number"] = acsn
result['serial_number'] = self._get_serial_number() result["serial_number"] = self._get_serial_number()
result['extensions_by_oid'] = self._get_all_extensions() result["extensions_by_oid"] = self._get_all_extensions()
result['ocsp_uri'] = self._get_ocsp_uri() result["ocsp_uri"] = self._get_ocsp_uri()
result['issuer_uri'] = self._get_issuer_uri() result["issuer_uri"] = self._get_issuer_uri()
return result return result
class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval): class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
"""Validate the supplied cert, using the cryptography backend""" """Validate the supplied cert, using the cryptography backend"""
def __init__(self, module, content): def __init__(self, module, content):
super(CertificateInfoRetrievalCryptography, self).__init__(module, 'cryptography', content) super(CertificateInfoRetrievalCryptography, self).__init__(
self.name_encoding = module.params.get('name_encoding', 'ignore') module, "cryptography", content
)
self.name_encoding = module.params.get("name_encoding", "ignore")
def _get_der_bytes(self): def _get_der_bytes(self):
return self.cert.public_bytes(serialization.Encoding.DER) return self.cert.public_bytes(serialization.Encoding.DER)
@@ -248,7 +271,9 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
def _get_key_usage(self): def _get_key_usage(self):
try: try:
current_key_ext = self.cert.extensions.get_extension_for_class(x509.KeyUsage) current_key_ext = self.cert.extensions.get_extension_for_class(
x509.KeyUsage
)
current_key_usage = current_key_ext.value current_key_usage = current_key_ext.value
key_usage = dict( key_usage = dict(
digital_signature=current_key_usage.digital_signature, digital_signature=current_key_usage.digital_signature,
@@ -261,45 +286,63 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
encipher_only=False, encipher_only=False,
decipher_only=False, decipher_only=False,
) )
if key_usage['key_agreement']: if key_usage["key_agreement"]:
key_usage.update(dict( key_usage.update(
dict(
encipher_only=current_key_usage.encipher_only, encipher_only=current_key_usage.encipher_only,
decipher_only=current_key_usage.decipher_only decipher_only=current_key_usage.decipher_only,
)) )
)
key_usage_names = dict( key_usage_names = dict(
digital_signature='Digital Signature', digital_signature="Digital Signature",
content_commitment='Non Repudiation', content_commitment="Non Repudiation",
key_encipherment='Key Encipherment', key_encipherment="Key Encipherment",
data_encipherment='Data Encipherment', data_encipherment="Data Encipherment",
key_agreement='Key Agreement', key_agreement="Key Agreement",
key_cert_sign='Certificate Sign', key_cert_sign="Certificate Sign",
crl_sign='CRL Sign', crl_sign="CRL Sign",
encipher_only='Encipher Only', encipher_only="Encipher Only",
decipher_only='Decipher Only', decipher_only="Decipher Only",
)
return (
sorted(
[
key_usage_names[name]
for name, value in key_usage.items()
if value
]
),
current_key_ext.critical,
) )
return sorted([
key_usage_names[name] for name, value in key_usage.items() if value
]), current_key_ext.critical
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
return None, False return None, False
def _get_extended_key_usage(self): def _get_extended_key_usage(self):
try: try:
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage) ext_keyusage_ext = self.cert.extensions.get_extension_for_class(
return sorted([ x509.ExtendedKeyUsage
cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value )
]), ext_keyusage_ext.critical return (
sorted(
[cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value]
),
ext_keyusage_ext.critical,
)
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
return None, False return None, False
def _get_basic_constraints(self): def _get_basic_constraints(self):
try: try:
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.BasicConstraints) ext_keyusage_ext = self.cert.extensions.get_extension_for_class(
x509.BasicConstraints
)
result = [] result = []
result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE')) result.append(
"CA:{0}".format("TRUE" if ext_keyusage_ext.value.ca else "FALSE")
)
if ext_keyusage_ext.value.path_length is not None: if ext_keyusage_ext.value.path_length is not None:
result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length)) result.append("pathlen:{0}".format(ext_keyusage_ext.value.path_length))
return sorted(result), ext_keyusage_ext.critical return sorted(result), ext_keyusage_ext.critical
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
return None, False return None, False
@@ -308,8 +351,13 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
try: try:
try: try:
# This only works with cryptography >= 2.1 # This only works with cryptography >= 2.1
tlsfeature_ext = self.cert.extensions.get_extension_for_class(x509.TLSFeature) tlsfeature_ext = self.cert.extensions.get_extension_for_class(
value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value x509.TLSFeature
)
value = (
cryptography.x509.TLSFeatureType.status_request
in tlsfeature_ext.value
)
except AttributeError: except AttributeError:
# Fallback for cryptography < 2.1 # Fallback for cryptography < 2.1
oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24") oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
@@ -321,8 +369,13 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
def _get_subject_alt_name(self): def _get_subject_alt_name(self):
try: try:
san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) san_ext = self.cert.extensions.get_extension_for_class(
result = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in san_ext.value] x509.SubjectAlternativeName
)
result = [
cryptography_decode_name(san, idn_rewrite=self.name_encoding)
for san in san_ext.value
]
return result, san_ext.critical return result, san_ext.critical
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
return None, False return None, False
@@ -344,18 +397,29 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
def _get_subject_key_identifier(self): def _get_subject_key_identifier(self):
try: try:
ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) ext = self.cert.extensions.get_extension_for_class(
x509.SubjectKeyIdentifier
)
return ext.value.digest return ext.value.digest
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
return None return None
def _get_authority_key_identifier(self): def _get_authority_key_identifier(self):
try: try:
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) ext = self.cert.extensions.get_extension_for_class(
x509.AuthorityKeyIdentifier
)
issuer = None issuer = None
if ext.value.authority_cert_issuer is not None: if ext.value.authority_cert_issuer is not None:
issuer = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in ext.value.authority_cert_issuer] issuer = [
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number cryptography_decode_name(san, idn_rewrite=self.name_encoding)
for san in ext.value.authority_cert_issuer
]
return (
ext.value.key_identifier,
issuer,
ext.value.authority_cert_serial_number,
)
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
return None, None, None return None, None, None
@@ -367,51 +431,69 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
def _get_ocsp_uri(self): def _get_ocsp_uri(self):
try: try:
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess) ext = self.cert.extensions.get_extension_for_class(
x509.AuthorityInformationAccess
)
for desc in ext.value: for desc in ext.value:
if desc.access_method == x509.oid.AuthorityInformationAccessOID.OCSP: if desc.access_method == x509.oid.AuthorityInformationAccessOID.OCSP:
if isinstance(desc.access_location, x509.UniformResourceIdentifier): if isinstance(desc.access_location, x509.UniformResourceIdentifier):
return desc.access_location.value return desc.access_location.value
except x509.ExtensionNotFound as dummy: except x509.ExtensionNotFound:
pass pass
return None return None
def _get_issuer_uri(self): def _get_issuer_uri(self):
try: try:
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess) ext = self.cert.extensions.get_extension_for_class(
x509.AuthorityInformationAccess
)
for desc in ext.value: for desc in ext.value:
if desc.access_method == x509.oid.AuthorityInformationAccessOID.CA_ISSUERS: if (
desc.access_method
== x509.oid.AuthorityInformationAccessOID.CA_ISSUERS
):
if isinstance(desc.access_location, x509.UniformResourceIdentifier): if isinstance(desc.access_location, x509.UniformResourceIdentifier):
return desc.access_location.value return desc.access_location.value
except x509.ExtensionNotFound as dummy: except x509.ExtensionNotFound:
pass pass
return None return None
def get_certificate_info(module, backend, content, prefer_one_fingerprint=False): def get_certificate_info(module, backend, content, prefer_one_fingerprint=False):
if backend == 'cryptography': if backend == "cryptography":
info = CertificateInfoRetrievalCryptography(module, content) info = CertificateInfoRetrievalCryptography(module, content)
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint) return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
def select_backend(module, backend, content): def select_backend(module, backend, content):
if backend == 'auto': if backend == "auto":
# Detection what is possible # Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) can_use_cryptography = (
CRYPTOGRAPHY_FOUND
and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
)
# Try cryptography # Try cryptography
if can_use_cryptography: if can_use_cryptography:
backend = 'cryptography' backend = "cryptography"
# Success? # Success?
if backend == 'auto': if backend == "auto":
module.fail_json(msg=("Cannot detect any of the required Python libraries " module.fail_json(
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) msg=(
"Cannot detect any of the required Python libraries "
"cryptography (>= {0})"
).format(MINIMAL_CRYPTOGRAPHY_VERSION)
)
if backend == 'cryptography': if backend == "cryptography":
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), module.fail_json(
exception=CRYPTOGRAPHY_IMP_ERR) msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
return backend, CertificateInfoRetrievalCryptography(module, content) return backend, CertificateInfoRetrievalCryptography(module, content)
else: else:
raise ValueError('Unsupported value for backend: {0}'.format(backend)) raise ValueError("Unsupported value for backend: {0}".format(backend))

View File

@@ -6,25 +6,17 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import os import os
from random import randrange from random import randrange
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLBadPassphraseError, OpenSSLBadPassphraseError,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_privatekey,
load_certificate,
select_message_digest,
)
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_TIMEZONE, CRYPTOGRAPHY_TIMEZONE,
cryptography_compare_public_keys, cryptography_compare_public_keys,
@@ -36,17 +28,24 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
set_not_valid_after, set_not_valid_after,
set_not_valid_before, set_not_valid_before,
) )
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 (
CRYPTOGRAPHY_VERSION, CRYPTOGRAPHY_VERSION,
CertificateError,
CertificateBackend, CertificateBackend,
CertificateError,
CertificateProvider, CertificateProvider,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_certificate,
load_privatekey,
select_message_digest,
)
from ansible_collections.community.crypto.plugins.module_utils.time import ( from ansible_collections.community.crypto.plugins.module_utils.time import (
get_relative_time_option, get_relative_time_option,
) )
from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
try: try:
import cryptography import cryptography
@@ -59,75 +58,90 @@ except ImportError:
class OwnCACertificateBackendCryptography(CertificateBackend): class OwnCACertificateBackendCryptography(CertificateBackend):
def __init__(self, module): def __init__(self, module):
super(OwnCACertificateBackendCryptography, self).__init__(module, 'cryptography') super(OwnCACertificateBackendCryptography, self).__init__(
module, "cryptography"
)
self.create_subject_key_identifier = module.params['ownca_create_subject_key_identifier'] self.create_subject_key_identifier = module.params[
self.create_authority_key_identifier = module.params['ownca_create_authority_key_identifier'] "ownca_create_subject_key_identifier"
]
self.create_authority_key_identifier = module.params[
"ownca_create_authority_key_identifier"
]
self.notBefore = get_relative_time_option( self.notBefore = get_relative_time_option(
module.params['ownca_not_before'], module.params["ownca_not_before"],
'ownca_not_before', "ownca_not_before",
backend=self.backend, backend=self.backend,
with_timezone=CRYPTOGRAPHY_TIMEZONE, with_timezone=CRYPTOGRAPHY_TIMEZONE,
) )
self.notAfter = get_relative_time_option( self.notAfter = get_relative_time_option(
module.params['ownca_not_after'], module.params["ownca_not_after"],
'ownca_not_after', "ownca_not_after",
backend=self.backend, backend=self.backend,
with_timezone=CRYPTOGRAPHY_TIMEZONE, with_timezone=CRYPTOGRAPHY_TIMEZONE,
) )
self.digest = select_message_digest(module.params['ownca_digest']) self.digest = select_message_digest(module.params["ownca_digest"])
self.version = module.params['ownca_version'] self.version = module.params["ownca_version"]
self.serial_number = x509.random_serial_number() self.serial_number = x509.random_serial_number()
self.ca_cert_path = module.params['ownca_path'] self.ca_cert_path = module.params["ownca_path"]
self.ca_cert_content = module.params['ownca_content'] self.ca_cert_content = module.params["ownca_content"]
if self.ca_cert_content is not None: if self.ca_cert_content is not None:
self.ca_cert_content = self.ca_cert_content.encode('utf-8') self.ca_cert_content = self.ca_cert_content.encode("utf-8")
self.ca_privatekey_path = module.params['ownca_privatekey_path'] self.ca_privatekey_path = module.params["ownca_privatekey_path"]
self.ca_privatekey_content = module.params['ownca_privatekey_content'] self.ca_privatekey_content = module.params["ownca_privatekey_content"]
if self.ca_privatekey_content is not None: if self.ca_privatekey_content is not None:
self.ca_privatekey_content = self.ca_privatekey_content.encode('utf-8') self.ca_privatekey_content = self.ca_privatekey_content.encode("utf-8")
self.ca_privatekey_passphrase = module.params['ownca_privatekey_passphrase'] self.ca_privatekey_passphrase = module.params["ownca_privatekey_passphrase"]
if self.csr_content is None and self.csr_path is None: if self.csr_content is None and self.csr_path is None:
raise CertificateError( raise CertificateError(
'csr_path or csr_content is required for ownca provider' "csr_path or csr_content is required for ownca provider"
) )
if self.csr_content is None and not os.path.exists(self.csr_path): if self.csr_content is None and not os.path.exists(self.csr_path):
raise CertificateError( raise CertificateError(
'The certificate signing request file {0} does not exist'.format(self.csr_path) "The certificate signing request file {0} does not exist".format(
self.csr_path
)
) )
if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path): if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path):
raise CertificateError( raise CertificateError(
'The CA certificate file {0} does not exist'.format(self.ca_cert_path) "The CA certificate file {0} does not exist".format(self.ca_cert_path)
) )
if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path): if self.ca_privatekey_content is None and not os.path.exists(
self.ca_privatekey_path
):
raise CertificateError( raise CertificateError(
'The CA private key file {0} does not exist'.format(self.ca_privatekey_path) "The CA private key file {0} does not exist".format(
self.ca_privatekey_path
)
) )
self._ensure_csr_loaded() self._ensure_csr_loaded()
self.ca_cert = load_certificate( self.ca_cert = load_certificate(
path=self.ca_cert_path, path=self.ca_cert_path, content=self.ca_cert_content, backend=self.backend
content=self.ca_cert_content,
backend=self.backend
) )
try: try:
self.ca_private_key = load_privatekey( self.ca_private_key = load_privatekey(
path=self.ca_privatekey_path, path=self.ca_privatekey_path,
content=self.ca_privatekey_content, content=self.ca_privatekey_content,
passphrase=self.ca_privatekey_passphrase, passphrase=self.ca_privatekey_passphrase,
backend=self.backend backend=self.backend,
) )
except OpenSSLBadPassphraseError as exc: except OpenSSLBadPassphraseError as exc:
module.fail_json(msg=str(exc)) module.fail_json(msg=str(exc))
if not cryptography_compare_public_keys(self.ca_cert.public_key(), self.ca_private_key.public_key()): if not cryptography_compare_public_keys(
raise CertificateError('The CA private key does not belong to the CA certificate') self.ca_cert.public_key(), self.ca_private_key.public_key()
):
raise CertificateError(
"The CA private key does not belong to the CA certificate"
)
if cryptography_key_needs_digest_for_signing(self.ca_private_key): if cryptography_key_needs_digest_for_signing(self.ca_private_key):
if self.digest is None: if self.digest is None:
raise CertificateError( raise CertificateError(
'The digest %s is not supported with the cryptography backend' % module.params['ownca_digest'] "The digest %s is not supported with the cryptography backend"
% module.params["ownca_digest"]
) )
else: else:
self.digest = None self.digest = None
@@ -144,40 +158,60 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
has_ski = False has_ski = False
for extension in self.csr.extensions: for extension in self.csr.extensions:
if isinstance(extension.value, x509.SubjectKeyIdentifier): if isinstance(extension.value, x509.SubjectKeyIdentifier):
if self.create_subject_key_identifier == 'always_create': if self.create_subject_key_identifier == "always_create":
continue continue
has_ski = True has_ski = True
if self.create_authority_key_identifier and isinstance(extension.value, x509.AuthorityKeyIdentifier): if self.create_authority_key_identifier and isinstance(
extension.value, x509.AuthorityKeyIdentifier
):
continue continue
cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical) cert_builder = cert_builder.add_extension(
if not has_ski and self.create_subject_key_identifier != 'never_create': extension.value, critical=extension.critical
)
if not has_ski and self.create_subject_key_identifier != "never_create":
cert_builder = cert_builder.add_extension( cert_builder = cert_builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(self.csr.public_key()), x509.SubjectKeyIdentifier.from_public_key(self.csr.public_key()),
critical=False critical=False,
) )
if self.create_authority_key_identifier: if self.create_authority_key_identifier:
try: try:
ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) ext = self.ca_cert.extensions.get_extension_for_class(
x509.SubjectKeyIdentifier
)
cert_builder = cert_builder.add_extension( cert_builder = cert_builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value) (
if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext), ext.value
critical=False )
if CRYPTOGRAPHY_VERSION >= LooseVersion("2.7")
else x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
ext
)
),
critical=False,
) )
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
cert_builder = cert_builder.add_extension( cert_builder = cert_builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key()), x509.AuthorityKeyIdentifier.from_issuer_public_key(
critical=False self.ca_cert.public_key()
),
critical=False,
) )
try: try:
certificate = cert_builder.sign( certificate = cert_builder.sign(
private_key=self.ca_private_key, algorithm=self.digest, private_key=self.ca_private_key,
backend=default_backend() algorithm=self.digest,
backend=default_backend(),
) )
except TypeError as e: except TypeError as e:
if str(e) == 'Algorithm must be a registered hash algorithm.' and self.digest is None: if (
self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') str(e) == "Algorithm must be a registered hash algorithm."
and self.digest is None
):
self.module.fail_json(
msg="Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer."
)
raise raise
self.cert = certificate self.cert = certificate
@@ -187,13 +221,17 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
return self.cert.public_bytes(Encoding.PEM) return self.cert.public_bytes(Encoding.PEM)
def needs_regeneration(self): def needs_regeneration(self):
if super(OwnCACertificateBackendCryptography, self).needs_regeneration(not_before=self.notBefore, not_after=self.notAfter): if super(OwnCACertificateBackendCryptography, self).needs_regeneration(
not_before=self.notBefore, not_after=self.notAfter
):
return True return True
self._ensure_existing_certificate_loaded() self._ensure_existing_certificate_loaded()
# Check whether certificate is signed by CA certificate # Check whether certificate is signed by CA certificate
if not cryptography_verify_certificate_signature(self.existing_certificate, self.ca_cert.public_key()): if not cryptography_verify_certificate_signature(
self.existing_certificate, self.ca_cert.public_key()
):
return True return True
# Check subject # Check subject
@@ -203,45 +241,67 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
# Check AuthorityKeyIdentifier # Check AuthorityKeyIdentifier
if self.create_authority_key_identifier: if self.create_authority_key_identifier:
try: try:
ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) ext = self.ca_cert.extensions.get_extension_for_class(
x509.SubjectKeyIdentifier
)
expected_ext = ( expected_ext = (
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value) x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else ext.value
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext) )
if CRYPTOGRAPHY_VERSION >= LooseVersion("2.7")
else x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
ext
)
) )
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
expected_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key()) expected_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key(
self.ca_cert.public_key()
)
try: try:
ext = self.existing_certificate.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) ext = self.existing_certificate.extensions.get_extension_for_class(
x509.AuthorityKeyIdentifier
)
if ext.value != expected_ext: if ext.value != expected_ext:
return True return True
except cryptography.x509.ExtensionNotFound as dummy: except cryptography.x509.ExtensionNotFound:
return True return True
return False return False
def dump(self, include_certificate): def dump(self, include_certificate):
result = super(OwnCACertificateBackendCryptography, self).dump(include_certificate) result = super(OwnCACertificateBackendCryptography, self).dump(
result.update({ include_certificate
'ca_cert': self.ca_cert_path, )
'ca_privatekey': self.ca_privatekey_path, result.update(
}) {
"ca_cert": self.ca_cert_path,
"ca_privatekey": self.ca_privatekey_path,
}
)
if self.module.check_mode: if self.module.check_mode:
result.update({ result.update(
'notBefore': self.notBefore.strftime("%Y%m%d%H%M%SZ"), {
'notAfter': self.notAfter.strftime("%Y%m%d%H%M%SZ"), "notBefore": self.notBefore.strftime("%Y%m%d%H%M%SZ"),
'serial_number': self.serial_number, "notAfter": self.notAfter.strftime("%Y%m%d%H%M%SZ"),
}) "serial_number": self.serial_number,
}
)
else: else:
if self.cert is None: if self.cert is None:
self.cert = self.existing_certificate self.cert = self.existing_certificate
result.update({ result.update(
'notBefore': get_not_valid_before(self.cert).strftime("%Y%m%d%H%M%SZ"), {
'notAfter': get_not_valid_after(self.cert).strftime("%Y%m%d%H%M%SZ"), "notBefore": get_not_valid_before(self.cert).strftime(
'serial_number': cryptography_serial_number_of_cert(self.cert), "%Y%m%d%H%M%SZ"
}) ),
"notAfter": get_not_valid_after(self.cert).strftime(
"%Y%m%d%H%M%SZ"
),
"serial_number": cryptography_serial_number_of_cert(self.cert),
}
)
return result return result
@@ -256,39 +316,53 @@ def generate_serial_number():
class OwnCACertificateProvider(CertificateProvider): class OwnCACertificateProvider(CertificateProvider):
def validate_module_args(self, module): def validate_module_args(self, module):
if module.params['ownca_path'] is None and module.params['ownca_content'] is None: if (
module.fail_json(msg='One of ownca_path and ownca_content must be specified for the ownca provider.') module.params["ownca_path"] is None
if module.params['ownca_privatekey_path'] is None and module.params['ownca_privatekey_content'] is None: and module.params["ownca_content"] is None
module.fail_json(msg='One of ownca_privatekey_path and ownca_privatekey_content must be specified for the ownca provider.') ):
module.fail_json(
msg="One of ownca_path and ownca_content must be specified for the ownca provider."
)
if (
module.params["ownca_privatekey_path"] is None
and module.params["ownca_privatekey_content"] is None
):
module.fail_json(
msg="One of ownca_privatekey_path and ownca_privatekey_content must be specified for the ownca provider."
)
def needs_version_two_certs(self, module): def needs_version_two_certs(self, module):
return module.params['ownca_version'] == 2 return module.params["ownca_version"] == 2
def create_backend(self, module, backend): def create_backend(self, module, backend):
if backend == 'cryptography': if backend == "cryptography":
return OwnCACertificateBackendCryptography(module) return OwnCACertificateBackendCryptography(module)
def add_ownca_provider_to_argument_spec(argument_spec): def add_ownca_provider_to_argument_spec(argument_spec):
argument_spec.argument_spec['provider']['choices'].append('ownca') argument_spec.argument_spec["provider"]["choices"].append("ownca")
argument_spec.argument_spec.update(dict( argument_spec.argument_spec.update(
ownca_path=dict(type='path'), dict(
ownca_content=dict(type='str'), ownca_path=dict(type="path"),
ownca_privatekey_path=dict(type='path'), ownca_content=dict(type="str"),
ownca_privatekey_content=dict(type='str', no_log=True), ownca_privatekey_path=dict(type="path"),
ownca_privatekey_passphrase=dict(type='str', no_log=True), ownca_privatekey_content=dict(type="str", no_log=True),
ownca_digest=dict(type='str', default='sha256'), ownca_privatekey_passphrase=dict(type="str", no_log=True),
ownca_version=dict(type='int', default=3), ownca_digest=dict(type="str", default="sha256"),
ownca_not_before=dict(type='str', default='+0s'), ownca_version=dict(type="int", default=3),
ownca_not_after=dict(type='str', default='+3650d'), ownca_not_before=dict(type="str", default="+0s"),
ownca_not_after=dict(type="str", default="+3650d"),
ownca_create_subject_key_identifier=dict( ownca_create_subject_key_identifier=dict(
type='str', type="str",
default='create_if_not_provided', default="create_if_not_provided",
choices=['create_if_not_provided', 'always_create', 'never_create'] choices=["create_if_not_provided", "always_create", "never_create"],
), ),
ownca_create_authority_key_identifier=dict(type='bool', default=True), ownca_create_authority_key_identifier=dict(type="bool", default=True),
)) )
argument_spec.mutually_exclusive.extend([ )
['ownca_path', 'ownca_content'], argument_spec.mutually_exclusive.extend(
['ownca_privatekey_path', 'ownca_privatekey_content'], [
]) ["ownca_path", "ownca_content"],
["ownca_privatekey_path", "ownca_privatekey_content"],
]
)

View File

@@ -6,17 +6,14 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import os import os
from random import randrange from random import randrange
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
select_message_digest,
)
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_TIMEZONE, CRYPTOGRAPHY_TIMEZONE,
cryptography_key_needs_digest_for_signing, cryptography_key_needs_digest_for_signing,
@@ -27,17 +24,19 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
set_not_valid_after, set_not_valid_after,
set_not_valid_before, set_not_valid_before,
) )
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,
CertificateBackend, CertificateBackend,
CertificateError,
CertificateProvider, CertificateProvider,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
select_message_digest,
)
from ansible_collections.community.crypto.plugins.module_utils.time import ( from ansible_collections.community.crypto.plugins.module_utils.time import (
get_relative_time_option, get_relative_time_option,
) )
try: try:
import cryptography import cryptography
from cryptography import x509 from cryptography import x509
@@ -49,32 +48,38 @@ except ImportError:
class SelfSignedCertificateBackendCryptography(CertificateBackend): class SelfSignedCertificateBackendCryptography(CertificateBackend):
def __init__(self, module): def __init__(self, module):
super(SelfSignedCertificateBackendCryptography, self).__init__(module, 'cryptography') super(SelfSignedCertificateBackendCryptography, self).__init__(
module, "cryptography"
)
self.create_subject_key_identifier = module.params['selfsigned_create_subject_key_identifier'] self.create_subject_key_identifier = module.params[
"selfsigned_create_subject_key_identifier"
]
self.notBefore = get_relative_time_option( self.notBefore = get_relative_time_option(
module.params['selfsigned_not_before'], module.params["selfsigned_not_before"],
'selfsigned_not_before', "selfsigned_not_before",
backend=self.backend, backend=self.backend,
with_timezone=CRYPTOGRAPHY_TIMEZONE, with_timezone=CRYPTOGRAPHY_TIMEZONE,
) )
self.notAfter = get_relative_time_option( self.notAfter = get_relative_time_option(
module.params['selfsigned_not_after'], module.params["selfsigned_not_after"],
'selfsigned_not_after', "selfsigned_not_after",
backend=self.backend, backend=self.backend,
with_timezone=CRYPTOGRAPHY_TIMEZONE, with_timezone=CRYPTOGRAPHY_TIMEZONE,
) )
self.digest = select_message_digest(module.params['selfsigned_digest']) self.digest = select_message_digest(module.params["selfsigned_digest"])
self.version = module.params['selfsigned_version'] self.version = module.params["selfsigned_version"]
self.serial_number = x509.random_serial_number() self.serial_number = x509.random_serial_number()
if self.csr_path is not None and not os.path.exists(self.csr_path): if self.csr_path is not None and not os.path.exists(self.csr_path):
raise CertificateError( raise CertificateError(
'The certificate signing request file {0} does not exist'.format(self.csr_path) "The certificate signing request file {0} does not exist".format(
self.csr_path
)
) )
if self.privatekey_content is None and not os.path.exists(self.privatekey_path): if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
raise CertificateError( raise CertificateError(
'The private key file {0} does not exist'.format(self.privatekey_path) "The private key file {0} does not exist".format(self.privatekey_path)
) )
self._module = module self._module = module
@@ -90,18 +95,28 @@ class SelfSignedCertificateBackendCryptography(CertificateBackend):
if cryptography_key_needs_digest_for_signing(self.privatekey): if cryptography_key_needs_digest_for_signing(self.privatekey):
digest = self.digest digest = self.digest
if digest is None: if digest is None:
self.module.fail_json(msg='Unsupported digest "{0}"'.format(module.params['selfsigned_digest'])) self.module.fail_json(
msg='Unsupported digest "{0}"'.format(
module.params["selfsigned_digest"]
)
)
try: try:
self.csr = csr.sign(self.privatekey, digest, default_backend()) self.csr = csr.sign(self.privatekey, digest, default_backend())
except TypeError as e: except TypeError as e:
if str(e) == 'Algorithm must be a registered hash algorithm.' and digest is None: if (
self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') str(e) == "Algorithm must be a registered hash algorithm."
and digest is None
):
self.module.fail_json(
msg="Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer."
)
raise raise
if cryptography_key_needs_digest_for_signing(self.privatekey): if cryptography_key_needs_digest_for_signing(self.privatekey):
if self.digest is None: if self.digest is None:
raise CertificateError( raise CertificateError(
'The digest %s is not supported with the cryptography backend' % module.params['selfsigned_digest'] "The digest %s is not supported with the cryptography backend"
% module.params["selfsigned_digest"]
) )
else: else:
self.digest = None self.digest = None
@@ -119,26 +134,36 @@ class SelfSignedCertificateBackendCryptography(CertificateBackend):
has_ski = False has_ski = False
for extension in self.csr.extensions: for extension in self.csr.extensions:
if isinstance(extension.value, x509.SubjectKeyIdentifier): if isinstance(extension.value, x509.SubjectKeyIdentifier):
if self.create_subject_key_identifier == 'always_create': if self.create_subject_key_identifier == "always_create":
continue continue
has_ski = True has_ski = True
cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical)
if not has_ski and self.create_subject_key_identifier != 'never_create':
cert_builder = cert_builder.add_extension( cert_builder = cert_builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()), extension.value, critical=extension.critical
critical=False )
if not has_ski and self.create_subject_key_identifier != "never_create":
cert_builder = cert_builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(
self.privatekey.public_key()
),
critical=False,
) )
except ValueError as e: except ValueError as e:
raise CertificateError(str(e)) raise CertificateError(str(e))
try: try:
certificate = cert_builder.sign( certificate = cert_builder.sign(
private_key=self.privatekey, algorithm=self.digest, private_key=self.privatekey,
backend=default_backend() algorithm=self.digest,
backend=default_backend(),
) )
except TypeError as e: except TypeError as e:
if str(e) == 'Algorithm must be a registered hash algorithm.' and self.digest is None: if (
self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') str(e) == "Algorithm must be a registered hash algorithm."
and self.digest is None
):
self.module.fail_json(
msg="Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer."
)
raise raise
self.cert = certificate self.cert = certificate
@@ -148,34 +173,48 @@ class SelfSignedCertificateBackendCryptography(CertificateBackend):
return self.cert.public_bytes(Encoding.PEM) return self.cert.public_bytes(Encoding.PEM)
def needs_regeneration(self): def needs_regeneration(self):
if super(SelfSignedCertificateBackendCryptography, self).needs_regeneration(not_before=self.notBefore, not_after=self.notAfter): if super(SelfSignedCertificateBackendCryptography, self).needs_regeneration(
not_before=self.notBefore, not_after=self.notAfter
):
return True return True
self._ensure_existing_certificate_loaded() self._ensure_existing_certificate_loaded()
# Check whether certificate is signed by private key # Check whether certificate is signed by private key
if not cryptography_verify_certificate_signature(self.existing_certificate, self.privatekey.public_key()): if not cryptography_verify_certificate_signature(
self.existing_certificate, self.privatekey.public_key()
):
return True return True
return False return False
def dump(self, include_certificate): def dump(self, include_certificate):
result = super(SelfSignedCertificateBackendCryptography, self).dump(include_certificate) result = super(SelfSignedCertificateBackendCryptography, self).dump(
include_certificate
)
if self.module.check_mode: if self.module.check_mode:
result.update({ result.update(
'notBefore': self.notBefore.strftime("%Y%m%d%H%M%SZ"), {
'notAfter': self.notAfter.strftime("%Y%m%d%H%M%SZ"), "notBefore": self.notBefore.strftime("%Y%m%d%H%M%SZ"),
'serial_number': self.serial_number, "notAfter": self.notAfter.strftime("%Y%m%d%H%M%SZ"),
}) "serial_number": self.serial_number,
}
)
else: else:
if self.cert is None: if self.cert is None:
self.cert = self.existing_certificate self.cert = self.existing_certificate
result.update({ result.update(
'notBefore': get_not_valid_before(self.cert).strftime("%Y%m%d%H%M%SZ"), {
'notAfter': get_not_valid_after(self.cert).strftime("%Y%m%d%H%M%SZ"), "notBefore": get_not_valid_before(self.cert).strftime(
'serial_number': cryptography_serial_number_of_cert(self.cert), "%Y%m%d%H%M%SZ"
}) ),
"notAfter": get_not_valid_after(self.cert).strftime(
"%Y%m%d%H%M%SZ"
),
"serial_number": cryptography_serial_number_of_cert(self.cert),
}
)
return result return result
@@ -190,27 +229,38 @@ def generate_serial_number():
class SelfSignedCertificateProvider(CertificateProvider): class SelfSignedCertificateProvider(CertificateProvider):
def validate_module_args(self, module): def validate_module_args(self, module):
if module.params['privatekey_path'] is None and module.params['privatekey_content'] is None: if (
module.fail_json(msg='One of privatekey_path and privatekey_content must be specified for the selfsigned provider.') module.params["privatekey_path"] is None
and module.params["privatekey_content"] is None
):
module.fail_json(
msg="One of privatekey_path and privatekey_content must be specified for the selfsigned provider."
)
def needs_version_two_certs(self, module): def needs_version_two_certs(self, module):
return module.params['selfsigned_version'] == 2 return module.params["selfsigned_version"] == 2
def create_backend(self, module, backend): def create_backend(self, module, backend):
if backend == 'cryptography': if backend == "cryptography":
return SelfSignedCertificateBackendCryptography(module) return SelfSignedCertificateBackendCryptography(module)
def add_selfsigned_provider_to_argument_spec(argument_spec): def add_selfsigned_provider_to_argument_spec(argument_spec):
argument_spec.argument_spec['provider']['choices'].append('selfsigned') argument_spec.argument_spec["provider"]["choices"].append("selfsigned")
argument_spec.argument_spec.update(dict( argument_spec.argument_spec.update(
selfsigned_version=dict(type='int', default=3), dict(
selfsigned_digest=dict(type='str', default='sha256'), selfsigned_version=dict(type="int", default=3),
selfsigned_not_before=dict(type='str', default='+0s', aliases=['selfsigned_notBefore']), selfsigned_digest=dict(type="str", default="sha256"),
selfsigned_not_after=dict(type='str', default='+3650d', aliases=['selfsigned_notAfter']), selfsigned_not_before=dict(
selfsigned_create_subject_key_identifier=dict( type="str", default="+0s", aliases=["selfsigned_notBefore"]
type='str',
default='create_if_not_provided',
choices=['create_if_not_provided', 'always_create', 'never_create']
), ),
)) selfsigned_not_after=dict(
type="str", default="+3650d", aliases=["selfsigned_notAfter"]
),
selfsigned_create_subject_key_identifier=dict(
type="str",
default="create_if_not_provided",
choices=["create_if_not_provided", "always_create", "never_create"],
),
)
)

View File

@@ -5,24 +5,29 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.argspec import (
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec as _ArgumentSpec ArgumentSpec as _ArgumentSpec,
)
class ArgumentSpec(_ArgumentSpec): class ArgumentSpec(_ArgumentSpec):
def create_ansible_module_helper(self, clazz, args, **kwargs): def create_ansible_module_helper(self, clazz, args, **kwargs):
result = super(ArgumentSpec, self).create_ansible_module_helper(clazz, args, **kwargs) result = super(ArgumentSpec, self).create_ansible_module_helper(
clazz, args, **kwargs
)
result.deprecate( result.deprecate(
"The crypto.module_backends.common module utils is deprecated and will be removed from community.crypto 3.0.0." "The crypto.module_backends.common module utils is deprecated and will be removed from community.crypto 3.0.0."
" Use the argspec module utils from community.crypto instead.", " Use the argspec module utils from community.crypto instead.",
version='3.0.0', version="3.0.0",
collection_name='community.crypto', collection_name="community.crypto",
) )
return result return result
__all__ = ('AnsibleModule', 'ArgumentSpec') __all__ = ("AnsibleModule", "ArgumentSpec")

View File

@@ -5,39 +5,41 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import traceback import traceback
from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.basic import missing_required_lib
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
cryptography_oid_to_name,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import (
TIMESTAMP_FORMAT, TIMESTAMP_FORMAT,
cryptography_decode_revoked_certificate, cryptography_decode_revoked_certificate,
cryptography_dump_revoked, cryptography_dump_revoked,
cryptography_get_signature_algorithm_oid_from_crl, cryptography_get_signature_algorithm_oid_from_crl,
) )
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.pem import ( 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.version import (
LooseVersion,
)
# crypto_utils # crypto_utils
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2' MINIMAL_CRYPTOGRAPHY_VERSION = "1.2"
CRYPTOGRAPHY_IMP_ERR = None CRYPTOGRAPHY_IMP_ERR = None
try: try:
import cryptography import cryptography
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError: except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -52,7 +54,7 @@ class CRLInfoRetrieval(object):
self.module = module self.module = module
self.content = content self.content = content
self.list_revoked_certificates = list_revoked_certificates self.list_revoked_certificates = list_revoked_certificates
self.name_encoding = module.params.get('name_encoding', 'ignore') self.name_encoding = module.params.get("name_encoding", "ignore")
def get_info(self): def get_info(self):
self.crl_pem = identify_pem_format(self.content) self.crl_pem = identify_pem_format(self.content)
@@ -62,41 +64,51 @@ class CRLInfoRetrieval(object):
else: else:
self.crl = x509.load_der_x509_crl(self.content, default_backend()) self.crl = x509.load_der_x509_crl(self.content, default_backend())
except ValueError as e: except ValueError as e:
self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e)) self.module.fail_json(msg="Error while decoding CRL: {0}".format(e))
result = { result = {
'changed': False, "changed": False,
'format': 'pem' if self.crl_pem else 'der', "format": "pem" if self.crl_pem else "der",
'last_update': None, "last_update": None,
'next_update': None, "next_update": None,
'digest': None, "digest": None,
'issuer_ordered': None, "issuer_ordered": None,
'issuer': None, "issuer": None,
} }
result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT) result["last_update"] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
result['next_update'] = self.crl.next_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)) result["digest"] = cryptography_oid_to_name(
cryptography_get_signature_algorithm_oid_from_crl(self.crl)
)
issuer = [] issuer = []
for attribute in self.crl.issuer: for attribute in self.crl.issuer:
issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value]) issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value])
result['issuer_ordered'] = issuer result["issuer_ordered"] = issuer
result['issuer'] = {} result["issuer"] = {}
for k, v in issuer: for k, v in issuer:
result['issuer'][k] = v result["issuer"][k] = v
if self.list_revoked_certificates: if self.list_revoked_certificates:
result['revoked_certificates'] = [] result["revoked_certificates"] = []
for cert in self.crl: for cert in self.crl:
entry = cryptography_decode_revoked_certificate(cert) entry = cryptography_decode_revoked_certificate(cert)
result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding)) result["revoked_certificates"].append(
cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding)
)
return result return result
def get_crl_info(module, content, list_revoked_certificates=True): def get_crl_info(module, content, list_revoked_certificates=True):
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), module.fail_json(
exception=CRYPTOGRAPHY_IMP_ERR) msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
info = CRLInfoRetrieval(module, content, list_revoked_certificates=list_revoked_certificates) info = CRLInfoRetrieval(
module, content, list_revoked_certificates=list_revoked_certificates
)
return info.get_info() return info.get_info()

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -17,30 +19,30 @@ import traceback
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.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_certificate_request,
)
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_decode_name, cryptography_decode_name,
cryptography_get_extensions_from_csr, cryptography_get_extensions_from_csr,
cryptography_oid_to_name, cryptography_oid_to_name,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
get_publickey_info, get_publickey_info,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_certificate_request,
)
from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
MINIMAL_CRYPTOGRAPHY_VERSION = "1.3"
CRYPTOGRAPHY_IMP_ERR = None CRYPTOGRAPHY_IMP_ERR = None
try: try:
import cryptography import cryptography
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError: except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -115,67 +117,80 @@ class CSRInfoRetrieval(object):
def get_info(self, prefer_one_fingerprint=False): def get_info(self, prefer_one_fingerprint=False):
result = dict() result = dict()
self.csr = load_certificate_request(None, content=self.content, backend=self.backend) self.csr = load_certificate_request(
None, content=self.content, backend=self.backend
)
subject = self._get_subject_ordered() subject = self._get_subject_ordered()
result['subject'] = dict() result["subject"] = dict()
for k, v in subject: for k, v in subject:
result['subject'][k] = v result["subject"][k] = v
result['subject_ordered'] = subject result["subject_ordered"] = subject
result['key_usage'], result['key_usage_critical'] = self._get_key_usage() 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["extended_key_usage"], result["extended_key_usage_critical"] = (
result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints() self._get_extended_key_usage()
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["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_permitted"],
result['name_constraints_excluded'], result["name_constraints_excluded"],
result['name_constraints_critical'], result["name_constraints_critical"],
) = self._get_name_constraints() ) = self._get_name_constraints()
result['public_key'] = to_native(self._get_public_key_pem()) result["public_key"] = to_native(self._get_public_key_pem())
public_key_info = get_publickey_info( public_key_info = get_publickey_info(
self.module, self.module,
self.backend, self.backend,
key=self._get_public_key_object(), key=self._get_public_key_object(),
prefer_one_fingerprint=prefer_one_fingerprint) prefer_one_fingerprint=prefer_one_fingerprint,
result.update({ )
'public_key_type': public_key_info['type'], result.update(
'public_key_data': public_key_info['public_data'], {
'public_key_fingerprints': public_key_info['fingerprints'], "public_key_type": public_key_info["type"],
}) "public_key_data": public_key_info["public_data"],
"public_key_fingerprints": public_key_info["fingerprints"],
}
)
ski = self._get_subject_key_identifier() ski = self._get_subject_key_identifier()
if ski is not None: if ski is not None:
ski = to_native(binascii.hexlify(ski)) ski = to_native(binascii.hexlify(ski))
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)]) ski = ":".join([ski[i : i + 2] for i in range(0, len(ski), 2)])
result['subject_key_identifier'] = ski result["subject_key_identifier"] = ski
aki, aci, acsn = self._get_authority_key_identifier() aki, aci, acsn = self._get_authority_key_identifier()
if aki is not None: if aki is not None:
aki = to_native(binascii.hexlify(aki)) aki = to_native(binascii.hexlify(aki))
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)]) aki = ":".join([aki[i : i + 2] for i in range(0, len(aki), 2)])
result['authority_key_identifier'] = aki result["authority_key_identifier"] = aki
result['authority_cert_issuer'] = aci result["authority_cert_issuer"] = aci
result['authority_cert_serial_number'] = acsn result["authority_cert_serial_number"] = acsn
result['extensions_by_oid'] = self._get_all_extensions() result["extensions_by_oid"] = self._get_all_extensions()
result['signature_valid'] = self._is_signature_valid() result["signature_valid"] = self._is_signature_valid()
if self.validate_signature and not result['signature_valid']: if self.validate_signature and not result["signature_valid"]:
self.module.fail_json( self.module.fail_json(msg="CSR signature is invalid!", **result)
msg='CSR signature is invalid!',
**result
)
return result return result
class CSRInfoRetrievalCryptography(CSRInfoRetrieval): class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
"""Validate the supplied CSR, using the cryptography backend""" """Validate the supplied CSR, using the cryptography backend"""
def __init__(self, module, content, validate_signature): def __init__(self, module, content, validate_signature):
super(CSRInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, validate_signature) super(CSRInfoRetrievalCryptography, self).__init__(
self.name_encoding = module.params.get('name_encoding', 'ignore') module, "cryptography", content, validate_signature
)
self.name_encoding = module.params.get("name_encoding", "ignore")
def _get_subject_ordered(self): def _get_subject_ordered(self):
result = [] result = []
@@ -198,44 +213,60 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
encipher_only=False, encipher_only=False,
decipher_only=False, decipher_only=False,
) )
if key_usage['key_agreement']: if key_usage["key_agreement"]:
key_usage.update(dict( key_usage.update(
dict(
encipher_only=current_key_usage.encipher_only, encipher_only=current_key_usage.encipher_only,
decipher_only=current_key_usage.decipher_only decipher_only=current_key_usage.decipher_only,
)) )
)
key_usage_names = dict( key_usage_names = dict(
digital_signature='Digital Signature', digital_signature="Digital Signature",
content_commitment='Non Repudiation', content_commitment="Non Repudiation",
key_encipherment='Key Encipherment', key_encipherment="Key Encipherment",
data_encipherment='Data Encipherment', data_encipherment="Data Encipherment",
key_agreement='Key Agreement', key_agreement="Key Agreement",
key_cert_sign='Certificate Sign', key_cert_sign="Certificate Sign",
crl_sign='CRL Sign', crl_sign="CRL Sign",
encipher_only='Encipher Only', encipher_only="Encipher Only",
decipher_only='Decipher Only', decipher_only="Decipher Only",
)
return (
sorted(
[
key_usage_names[name]
for name, value in key_usage.items()
if value
]
),
current_key_ext.critical,
) )
return sorted([
key_usage_names[name] for name, value in key_usage.items() if value
]), current_key_ext.critical
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
return None, False return None, False
def _get_extended_key_usage(self): def _get_extended_key_usage(self):
try: try:
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.ExtendedKeyUsage) ext_keyusage_ext = self.csr.extensions.get_extension_for_class(
return sorted([ x509.ExtendedKeyUsage
cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value )
]), ext_keyusage_ext.critical return (
sorted(
[cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value]
),
ext_keyusage_ext.critical,
)
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
return None, False return None, False
def _get_basic_constraints(self): def _get_basic_constraints(self):
try: try:
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.BasicConstraints) ext_keyusage_ext = self.csr.extensions.get_extension_for_class(
result = ['CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE')] 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: if ext_keyusage_ext.value.path_length is not None:
result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length)) result.append("pathlen:{0}".format(ext_keyusage_ext.value.path_length))
return sorted(result), ext_keyusage_ext.critical return sorted(result), ext_keyusage_ext.critical
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
return None, False return None, False
@@ -244,8 +275,13 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
try: try:
try: try:
# This only works with cryptography >= 2.1 # This only works with cryptography >= 2.1
tlsfeature_ext = self.csr.extensions.get_extension_for_class(x509.TLSFeature) tlsfeature_ext = self.csr.extensions.get_extension_for_class(
value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value x509.TLSFeature
)
value = (
cryptography.x509.TLSFeatureType.status_request
in tlsfeature_ext.value
)
except AttributeError: except AttributeError:
# Fallback for cryptography < 2.1 # Fallback for cryptography < 2.1
oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24") oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
@@ -257,8 +293,13 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
def _get_subject_alt_name(self): def _get_subject_alt_name(self):
try: try:
san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName) san_ext = self.csr.extensions.get_extension_for_class(
result = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in san_ext.value] x509.SubjectAlternativeName
)
result = [
cryptography_decode_name(san, idn_rewrite=self.name_encoding)
for san in san_ext.value
]
return result, san_ext.critical return result, san_ext.critical
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
return None, False return None, False
@@ -266,8 +307,14 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
def _get_name_constraints(self): def _get_name_constraints(self):
try: try:
nc_ext = self.csr.extensions.get_extension_for_class(x509.NameConstraints) nc_ext = self.csr.extensions.get_extension_for_class(x509.NameConstraints)
permitted = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in nc_ext.value.permitted_subtrees or []] permitted = [
excluded = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in nc_ext.value.excluded_subtrees or []] cryptography_decode_name(san, idn_rewrite=self.name_encoding)
for san in nc_ext.value.permitted_subtrees or []
]
excluded = [
cryptography_decode_name(san, idn_rewrite=self.name_encoding)
for san in nc_ext.value.excluded_subtrees or []
]
return permitted, excluded, nc_ext.critical return permitted, excluded, nc_ext.critical
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
return None, None, False return None, None, False
@@ -290,11 +337,20 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
def _get_authority_key_identifier(self): def _get_authority_key_identifier(self):
try: try:
ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) ext = self.csr.extensions.get_extension_for_class(
x509.AuthorityKeyIdentifier
)
issuer = None issuer = None
if ext.value.authority_cert_issuer is not None: if ext.value.authority_cert_issuer is not None:
issuer = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in ext.value.authority_cert_issuer] issuer = [
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number cryptography_decode_name(san, idn_rewrite=self.name_encoding)
for san in ext.value.authority_cert_issuer
]
return (
ext.value.key_identifier,
issuer,
ext.value.authority_cert_serial_number,
)
except cryptography.x509.ExtensionNotFound: except cryptography.x509.ExtensionNotFound:
return None, None, None return None, None, None
@@ -305,30 +361,46 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
return self.csr.is_signature_valid return self.csr.is_signature_valid
def get_csr_info(module, backend, content, validate_signature=True, prefer_one_fingerprint=False): def get_csr_info(
if backend == 'cryptography': module, backend, content, validate_signature=True, prefer_one_fingerprint=False
info = CSRInfoRetrievalCryptography(module, content, validate_signature=validate_signature) ):
if backend == "cryptography":
info = CSRInfoRetrievalCryptography(
module, content, validate_signature=validate_signature
)
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint) return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
def select_backend(module, backend, content, validate_signature=True): def select_backend(module, backend, content, validate_signature=True):
if backend == 'auto': if backend == "auto":
# Detection what is possible # Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) can_use_cryptography = (
CRYPTOGRAPHY_FOUND
and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
)
# Try cryptography # Try cryptography
if can_use_cryptography: if can_use_cryptography:
backend = 'cryptography' backend = "cryptography"
# Success? # Success?
if backend == 'auto': if backend == "auto":
module.fail_json(msg=("Cannot detect the required Python library " module.fail_json(
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) msg=(
"Cannot detect the required Python library " "cryptography (>= {0})"
).format(MINIMAL_CRYPTOGRAPHY_VERSION)
)
if backend == 'cryptography': if backend == "cryptography":
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), module.fail_json(
exception=CRYPTOGRAPHY_IMP_ERR) msg=missing_required_lib(
return backend, CSRInfoRetrievalCryptography(module, content, validate_signature=validate_signature) "cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
return backend, CSRInfoRetrievalCryptography(
module, content, validate_signature=validate_signature
)
else: else:
raise ValueError('Unsupported value for backend: {0}'.format(backend)) raise ValueError("Unsupported value for backend: {0}".format(backend))

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -16,47 +18,46 @@ import traceback
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.common.text.converters import to_bytes from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.argspec import (
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec ArgumentSpec,
)
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
CRYPTOGRAPHY_HAS_ED448,
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_X448,
CRYPTOGRAPHY_HAS_X25519, CRYPTOGRAPHY_HAS_X25519,
CRYPTOGRAPHY_HAS_X25519_FULL, CRYPTOGRAPHY_HAS_X25519_FULL,
CRYPTOGRAPHY_HAS_X448,
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_ED448,
OpenSSLObjectError, OpenSSLObjectError,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
get_fingerprint_of_privatekey,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
identify_private_key_format,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import (
PrivateKeyConsistencyError, PrivateKeyConsistencyError,
PrivateKeyParseError, PrivateKeyParseError,
get_privatekey_info, get_privatekey_info,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
identify_private_key_format,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
get_fingerprint_of_privatekey,
)
from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' MINIMAL_CRYPTOGRAPHY_VERSION = "1.2.3"
CRYPTOGRAPHY_IMP_ERR = None CRYPTOGRAPHY_IMP_ERR = None
try: try:
import cryptography import cryptography
import cryptography.exceptions import cryptography.exceptions
import cryptography.hazmat.backends import cryptography.hazmat.backends
import cryptography.hazmat.primitives.serialization
import cryptography.hazmat.primitives.asymmetric.rsa
import cryptography.hazmat.primitives.asymmetric.dsa import cryptography.hazmat.primitives.asymmetric.dsa
import cryptography.hazmat.primitives.asymmetric.ec import cryptography.hazmat.primitives.asymmetric.ec
import cryptography.hazmat.primitives.asymmetric.rsa
import cryptography.hazmat.primitives.asymmetric.utils import cryptography.hazmat.primitives.asymmetric.utils
import cryptography.hazmat.primitives.serialization
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError: except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -80,14 +81,14 @@ class PrivateKeyError(OpenSSLObjectError):
class PrivateKeyBackend: class PrivateKeyBackend:
def __init__(self, module, backend): def __init__(self, module, backend):
self.module = module self.module = module
self.type = module.params['type'] self.type = module.params["type"]
self.size = module.params['size'] self.size = module.params["size"]
self.curve = module.params['curve'] self.curve = module.params["curve"]
self.passphrase = module.params['passphrase'] self.passphrase = module.params["passphrase"]
self.cipher = module.params['cipher'] self.cipher = module.params["cipher"]
self.format = module.params['format'] self.format = module.params["format"]
self.format_mismatch = module.params.get('format_mismatch', 'regenerate') self.format_mismatch = module.params.get("format_mismatch", "regenerate")
self.regenerate = module.params.get('regenerate', 'full_idempotence') self.regenerate = module.params.get("regenerate", "full_idempotence")
self.backend = backend self.backend = backend
self.private_key = None self.private_key = None
@@ -103,14 +104,21 @@ class PrivateKeyBackend:
return dict() return dict()
result = dict(can_parse_key=False) result = dict(can_parse_key=False)
try: try:
result.update(get_privatekey_info( result.update(
self.module, self.backend, data, passphrase=self.passphrase, get_privatekey_info(
return_private_key_data=False, prefer_one_fingerprint=True)) self.module,
self.backend,
data,
passphrase=self.passphrase,
return_private_key_data=False,
prefer_one_fingerprint=True,
)
)
except PrivateKeyConsistencyError as exc: except PrivateKeyConsistencyError as exc:
result.update(exc.result) result.update(exc.result)
except PrivateKeyParseError as exc: except PrivateKeyParseError as exc:
result.update(exc.result) result.update(exc.result)
except Exception as exc: except Exception:
pass pass
return result return result
@@ -137,7 +145,9 @@ 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) 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."""
@@ -165,52 +175,64 @@ class PrivateKeyBackend:
def needs_regeneration(self): def needs_regeneration(self):
"""Check whether a regeneration is necessary.""" """Check whether a regeneration is necessary."""
if self.regenerate == 'always': if self.regenerate == "always":
return True return True
if not self.has_existing(): if not self.has_existing():
# key does not exist # key does not exist
return True return True
if not self._check_passphrase(): if not self._check_passphrase():
if self.regenerate == 'full_idempotence': if self.regenerate == "full_idempotence":
return True return True
self.module.fail_json(msg='Unable to read the key. The key is protected with a another passphrase / no passphrase or broken.' self.module.fail_json(
' Will not proceed. To force regeneration, call the module with `generate`' msg="Unable to read the key. The key is protected with a another passphrase / no passphrase or broken."
' set to `full_idempotence` or `always`, or with `force=true`.') " Will not proceed. To force regeneration, call the module with `generate`"
" set to `full_idempotence` or `always`, or with `force=true`."
)
self._ensure_existing_private_key_loaded() self._ensure_existing_private_key_loaded()
if self.regenerate != 'never': if self.regenerate != "never":
if not self._check_size_and_type(): if not self._check_size_and_type():
if self.regenerate in ('partial_idempotence', 'full_idempotence'): if self.regenerate in ("partial_idempotence", "full_idempotence"):
return True return True
self.module.fail_json(msg='Key has wrong type and/or size.' self.module.fail_json(
' Will not proceed. To force regeneration, call the module with `generate`' msg="Key has wrong type and/or size."
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=true`.') " Will not proceed. To force regeneration, call the module with `generate`"
" set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=true`."
)
# During generation step, regenerate if format does not match and format_mismatch == 'regenerate' # During generation step, regenerate if format does not match and format_mismatch == 'regenerate'
if self.format_mismatch == 'regenerate' and self.regenerate != 'never': if self.format_mismatch == "regenerate" and self.regenerate != "never":
if not self._check_format(): if not self._check_format():
if self.regenerate in ('partial_idempotence', 'full_idempotence'): if self.regenerate in ("partial_idempotence", "full_idempotence"):
return True return True
self.module.fail_json(msg='Key has wrong format.' self.module.fail_json(
' Will not proceed. To force regeneration, call the module with `generate`' msg="Key has wrong format."
' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=true`.' " Will not proceed. To force regeneration, call the module with `generate`"
' To convert the key, set `format_mismatch` to `convert`.') " set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=true`."
" To convert the key, set `format_mismatch` to `convert`."
)
return False return False
def needs_conversion(self): def needs_conversion(self):
"""Check whether a conversion is necessary. Must only be called if needs_regeneration() returned False.""" """Check whether a conversion is necessary. Must only be called if needs_regeneration() returned False."""
# During conversion step, convert if format does not match and format_mismatch == 'convert' # During conversion step, convert if format does not match and format_mismatch == 'convert'
self._ensure_existing_private_key_loaded() self._ensure_existing_private_key_loaded()
return self.has_existing() and self.format_mismatch == 'convert' and not self._check_format() return (
self.has_existing()
and self.format_mismatch == "convert"
and not self._check_format()
)
def _get_fingerprint(self): def _get_fingerprint(self):
if self.private_key: if self.private_key:
return get_fingerprint_of_privatekey(self.private_key, backend=self.backend) return get_fingerprint_of_privatekey(self.private_key, backend=self.backend)
try: try:
self._ensure_existing_private_key_loaded() self._ensure_existing_private_key_loaded()
except Exception as dummy: except Exception:
# Ignore errors # Ignore errors
pass pass
if self.existing_private_key: if self.existing_private_key:
return get_fingerprint_of_privatekey(self.existing_private_key, backend=self.backend) return get_fingerprint_of_privatekey(
self.existing_private_key, backend=self.backend
)
def dump(self, include_key): def dump(self, include_key):
"""Serialize the object into a dictionary.""" """Serialize the object into a dictionary."""
@@ -218,16 +240,16 @@ class PrivateKeyBackend:
if not self.private_key: if not self.private_key:
try: try:
self._ensure_existing_private_key_loaded() self._ensure_existing_private_key_loaded()
except Exception as dummy: except Exception:
# Ignore errors # Ignore errors
pass pass
result = { result = {
'type': self.type, "type": self.type,
'size': self.size, "size": self.size,
'fingerprint': self._get_fingerprint(), "fingerprint": self._get_fingerprint(),
} }
if self.type == 'ECC': if self.type == "ECC":
result['curve'] = self.curve result["curve"] = self.curve
# Get hold of private key bytes # Get hold of private key bytes
pk_bytes = self.existing_private_key_bytes pk_bytes = self.existing_private_key_bytes
if self.private_key is not None: if self.private_key is not None:
@@ -236,14 +258,14 @@ class PrivateKeyBackend:
if include_key: if include_key:
# 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":
result['privatekey'] = base64.b64encode(pk_bytes) result["privatekey"] = base64.b64encode(pk_bytes)
else: else:
result['privatekey'] = pk_bytes.decode('utf-8') result["privatekey"] = pk_bytes.decode("utf-8")
else: else:
result['privatekey'] = None result["privatekey"] = None
result['diff'] = dict( result["diff"] = dict(
before=self.diff_before, before=self.diff_before,
after=self.diff_after, after=self.diff_after,
) )
@@ -256,7 +278,9 @@ class PrivateKeyCryptographyBackend(PrivateKeyBackend):
def _get_ec_class(self, ectype): def _get_ec_class(self, ectype):
ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(ectype) ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(ectype)
if ecclass is None: if ecclass is None:
self.module.fail_json(msg='Your cryptography version does not support {0}'.format(ectype)) self.module.fail_json(
msg="Your cryptography version does not support {0}".format(ectype)
)
return ecclass return ecclass
def _add_curve(self, name, ectype, deprecated=False): def _add_curve(self, name, ectype, deprecated=False):
@@ -266,90 +290,123 @@ class PrivateKeyCryptographyBackend(PrivateKeyBackend):
def verify(privatekey): def verify(privatekey):
ecclass = self._get_ec_class(ectype) ecclass = self._get_ec_class(ectype)
return isinstance(privatekey.private_numbers().public_numbers.curve, ecclass) return isinstance(
privatekey.private_numbers().public_numbers.curve, ecclass
)
self.curves[name] = { self.curves[name] = {
'create': create, "create": create,
'verify': verify, "verify": verify,
'deprecated': deprecated, "deprecated": deprecated,
} }
def __init__(self, module): def __init__(self, module):
super(PrivateKeyCryptographyBackend, self).__init__(module=module, backend='cryptography') super(PrivateKeyCryptographyBackend, self).__init__(
module=module, backend="cryptography"
)
self.curves = dict() self.curves = dict()
self._add_curve('secp224r1', 'SECP224R1') self._add_curve("secp224r1", "SECP224R1")
self._add_curve('secp256k1', 'SECP256K1') self._add_curve("secp256k1", "SECP256K1")
self._add_curve('secp256r1', 'SECP256R1') self._add_curve("secp256r1", "SECP256R1")
self._add_curve('secp384r1', 'SECP384R1') self._add_curve("secp384r1", "SECP384R1")
self._add_curve('secp521r1', 'SECP521R1') self._add_curve("secp521r1", "SECP521R1")
self._add_curve('secp192r1', 'SECP192R1', deprecated=True) self._add_curve("secp192r1", "SECP192R1", deprecated=True)
self._add_curve('sect163k1', 'SECT163K1', deprecated=True) self._add_curve("sect163k1", "SECT163K1", deprecated=True)
self._add_curve('sect163r2', 'SECT163R2', deprecated=True) self._add_curve("sect163r2", "SECT163R2", deprecated=True)
self._add_curve('sect233k1', 'SECT233K1', deprecated=True) self._add_curve("sect233k1", "SECT233K1", deprecated=True)
self._add_curve('sect233r1', 'SECT233R1', deprecated=True) self._add_curve("sect233r1", "SECT233R1", deprecated=True)
self._add_curve('sect283k1', 'SECT283K1', deprecated=True) self._add_curve("sect283k1", "SECT283K1", deprecated=True)
self._add_curve('sect283r1', 'SECT283R1', deprecated=True) self._add_curve("sect283r1", "SECT283R1", deprecated=True)
self._add_curve('sect409k1', 'SECT409K1', deprecated=True) self._add_curve("sect409k1", "SECT409K1", deprecated=True)
self._add_curve('sect409r1', 'SECT409R1', deprecated=True) self._add_curve("sect409r1", "SECT409R1", deprecated=True)
self._add_curve('sect571k1', 'SECT571K1', deprecated=True) self._add_curve("sect571k1", "SECT571K1", deprecated=True)
self._add_curve('sect571r1', 'SECT571R1', deprecated=True) self._add_curve("sect571r1", "SECT571R1", deprecated=True)
self._add_curve('brainpoolP256r1', 'BrainpoolP256R1', deprecated=True) self._add_curve("brainpoolP256r1", "BrainpoolP256R1", deprecated=True)
self._add_curve('brainpoolP384r1', 'BrainpoolP384R1', deprecated=True) self._add_curve("brainpoolP384r1", "BrainpoolP384R1", deprecated=True)
self._add_curve('brainpoolP512r1', 'BrainpoolP512R1', deprecated=True) self._add_curve("brainpoolP512r1", "BrainpoolP512R1", deprecated=True)
self.cryptography_backend = cryptography.hazmat.backends.default_backend() self.cryptography_backend = cryptography.hazmat.backends.default_backend()
if not CRYPTOGRAPHY_HAS_X25519 and self.type == 'X25519': if not CRYPTOGRAPHY_HAS_X25519 and self.type == "X25519":
self.module.fail_json(msg='Your cryptography version does not support X25519') self.module.fail_json(
if not CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519': msg="Your cryptography version does not support X25519"
self.module.fail_json(msg='Your cryptography version does not support X25519 serialization') )
if not CRYPTOGRAPHY_HAS_X448 and self.type == 'X448': if not CRYPTOGRAPHY_HAS_X25519_FULL and self.type == "X25519":
self.module.fail_json(msg='Your cryptography version does not support X448') self.module.fail_json(
if not CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519': msg="Your cryptography version does not support X25519 serialization"
self.module.fail_json(msg='Your cryptography version does not support Ed25519') )
if not CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448': if not CRYPTOGRAPHY_HAS_X448 and self.type == "X448":
self.module.fail_json(msg='Your cryptography version does not support Ed448') self.module.fail_json(msg="Your cryptography version does not support X448")
if not CRYPTOGRAPHY_HAS_ED25519 and self.type == "Ed25519":
self.module.fail_json(
msg="Your cryptography version does not support Ed25519"
)
if not CRYPTOGRAPHY_HAS_ED448 and self.type == "Ed448":
self.module.fail_json(
msg="Your cryptography version does not support Ed448"
)
def _get_wanted_format(self): def _get_wanted_format(self):
if self.format not in ('auto', 'auto_ignore'): if self.format not in ("auto", "auto_ignore"):
return self.format return self.format
if self.type in ('X25519', 'X448', 'Ed25519', 'Ed448'): if self.type in ("X25519", "X448", "Ed25519", "Ed448"):
return 'pkcs8' return "pkcs8"
else: else:
return 'pkcs1' return "pkcs1"
def generate_private_key(self): def generate_private_key(self):
"""(Re-)Generate private key.""" """(Re-)Generate private key."""
try: try:
if self.type == 'RSA': if self.type == "RSA":
self.private_key = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key( self.private_key = (
cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key(
public_exponent=65537, # OpenSSL always uses this public_exponent=65537, # OpenSSL always uses this
key_size=self.size, key_size=self.size,
backend=self.cryptography_backend backend=self.cryptography_backend,
) )
if self.type == 'DSA':
self.private_key = cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key(
key_size=self.size,
backend=self.cryptography_backend
) )
if CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519': if self.type == "DSA":
self.private_key = cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate() self.private_key = (
if CRYPTOGRAPHY_HAS_X448 and self.type == 'X448': cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key(
self.private_key = cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate() key_size=self.size, backend=self.cryptography_backend
if CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519': )
self.private_key = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate() )
if CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448': if CRYPTOGRAPHY_HAS_X25519_FULL and self.type == "X25519":
self.private_key = cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.generate() self.private_key = (
if self.type == 'ECC' and self.curve in self.curves: cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate()
if self.curves[self.curve]['deprecated']: )
self.module.warn('Elliptic curves of type {0} should not be used for new keys!'.format(self.curve)) if CRYPTOGRAPHY_HAS_X448 and self.type == "X448":
self.private_key = cryptography.hazmat.primitives.asymmetric.ec.generate_private_key( self.private_key = (
curve=self.curves[self.curve]['create'](self.size), cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate()
backend=self.cryptography_backend )
if CRYPTOGRAPHY_HAS_ED25519 and self.type == "Ed25519":
self.private_key = (
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate()
)
if CRYPTOGRAPHY_HAS_ED448 and self.type == "Ed448":
self.private_key = (
cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.generate()
)
if self.type == "ECC" and self.curve in self.curves:
if self.curves[self.curve]["deprecated"]:
self.module.warn(
"Elliptic curves of type {0} should not be used for new keys!".format(
self.curve
)
)
self.private_key = (
cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(
curve=self.curves[self.curve]["create"](self.size),
backend=self.cryptography_backend,
)
)
except cryptography.exceptions.UnsupportedAlgorithm:
self.module.fail_json(
msg="Cryptography backend does not support the algorithm required for {0}".format(
self.type
)
) )
except cryptography.exceptions.UnsupportedAlgorithm as dummy:
self.module.fail_json(msg='Cryptography backend does not support the algorithm required for {0}'.format(self.type))
def get_private_key_data(self): def get_private_key_data(self):
"""Return bytes for self.private_key""" """Return bytes for self.private_key"""
@@ -357,40 +414,62 @@ class PrivateKeyCryptographyBackend(PrivateKeyBackend):
try: try:
export_format = self._get_wanted_format() export_format = self._get_wanted_format()
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM
if export_format == 'pkcs1': if export_format == "pkcs1":
# "TraditionalOpenSSL" format is PKCS1 # "TraditionalOpenSSL" format is PKCS1
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL export_format = (
elif export_format == 'pkcs8': cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8 )
elif export_format == 'raw': elif export_format == "pkcs8":
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.Raw export_format = (
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.Raw cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
)
elif export_format == "raw":
export_format = (
cryptography.hazmat.primitives.serialization.PrivateFormat.Raw
)
export_encoding = (
cryptography.hazmat.primitives.serialization.Encoding.Raw
)
except AttributeError: except AttributeError:
self.module.fail_json(msg='Cryptography backend does not support the selected output format "{0}"'.format(self.format)) self.module.fail_json(
msg='Cryptography backend does not support the selected output format "{0}"'.format(
self.format
)
)
# Select key encryption # Select key encryption
encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption() encryption_algorithm = (
cryptography.hazmat.primitives.serialization.NoEncryption()
)
if self.cipher and self.passphrase: if self.cipher and self.passphrase:
if self.cipher == 'auto': if self.cipher == "auto":
encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(to_bytes(self.passphrase)) encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(
to_bytes(self.passphrase)
)
else: else:
self.module.fail_json(msg='Cryptography backend can only use "auto" for cipher option.') self.module.fail_json(
msg='Cryptography backend can only use "auto" for cipher option.'
)
# Serialize key # Serialize key
try: try:
return self.private_key.private_bytes( return self.private_key.private_bytes(
encoding=export_encoding, encoding=export_encoding,
format=export_format, format=export_format,
encryption_algorithm=encryption_algorithm encryption_algorithm=encryption_algorithm,
) )
except ValueError as dummy: except ValueError:
self.module.fail_json( self.module.fail_json(
msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(self.format) msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(
self.format
) )
except Exception as dummy: )
except Exception:
self.module.fail_json( self.module.fail_json(
msg='Error while serializing the private key in the required format "{0}"'.format(self.format), msg='Error while serializing the private key in the required format "{0}"'.format(
exception=traceback.format_exc() self.format
),
exception=traceback.format_exc(),
) )
def _load_privatekey(self): def _load_privatekey(self):
@@ -398,27 +477,45 @@ class PrivateKeyCryptographyBackend(PrivateKeyBackend):
try: try:
# Interpret bytes depending on format. # Interpret bytes depending on format.
format = identify_private_key_format(data) format = identify_private_key_format(data)
if format == 'raw': if format == "raw":
if len(data) == 56 and CRYPTOGRAPHY_HAS_X448: if len(data) == 56 and CRYPTOGRAPHY_HAS_X448:
return cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data) return cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(
data
)
if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448: if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448:
return cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data) return cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(
data
)
if len(data) == 32: if len(data) == 32:
if CRYPTOGRAPHY_HAS_X25519 and (self.type == 'X25519' or not CRYPTOGRAPHY_HAS_ED25519): if CRYPTOGRAPHY_HAS_X25519 and (
return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) self.type == "X25519" or not CRYPTOGRAPHY_HAS_ED25519
if CRYPTOGRAPHY_HAS_ED25519 and (self.type == 'Ed25519' or not CRYPTOGRAPHY_HAS_X25519): ):
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(
data
)
if CRYPTOGRAPHY_HAS_ED25519 and (
self.type == "Ed25519" or not CRYPTOGRAPHY_HAS_X25519
):
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(
data
)
if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519: if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519:
try: try:
return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(
data
)
except Exception: except Exception:
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(
raise PrivateKeyError('Cannot load raw key') data
)
raise PrivateKeyError("Cannot load raw key")
else: else:
return cryptography.hazmat.primitives.serialization.load_pem_private_key( return (
cryptography.hazmat.primitives.serialization.load_pem_private_key(
data, data,
None if self.passphrase is None else to_bytes(self.passphrase), None if self.passphrase is None else to_bytes(self.passphrase),
backend=self.cryptography_backend backend=self.cryptography_backend,
)
) )
except Exception as e: except Exception as e:
raise PrivateKeyError(e) raise PrivateKeyError(e)
@@ -430,7 +527,7 @@ class PrivateKeyCryptographyBackend(PrivateKeyBackend):
def _check_passphrase(self): def _check_passphrase(self):
try: try:
format = identify_private_key_format(self.existing_private_key_bytes) format = identify_private_key_format(self.existing_private_key_bytes)
if format == 'raw': if format == "raw":
# Raw keys cannot be encrypted. To avoid incompatibilities, we try to # Raw keys cannot be encrypted. To avoid incompatibilities, we try to
# actually load the key (and return False when this fails). # actually load the key (and return False when this fails).
self._load_privatekey() self._load_privatekey()
@@ -438,93 +535,164 @@ class PrivateKeyCryptographyBackend(PrivateKeyBackend):
# provided. # provided.
return self.passphrase is None return self.passphrase is None
else: else:
return cryptography.hazmat.primitives.serialization.load_pem_private_key( return (
cryptography.hazmat.primitives.serialization.load_pem_private_key(
self.existing_private_key_bytes, self.existing_private_key_bytes,
None if self.passphrase is None else to_bytes(self.passphrase), None if self.passphrase is None else to_bytes(self.passphrase),
backend=self.cryptography_backend backend=self.cryptography_backend,
) )
except Exception as dummy: )
except Exception:
return False return False
def _check_size_and_type(self): def _check_size_and_type(self):
if isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): if isinstance(
return self.type == 'RSA' and self.size == self.existing_private_key.key_size self.existing_private_key,
if isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey,
return self.type == 'DSA' and self.size == self.existing_private_key.key_size ):
if CRYPTOGRAPHY_HAS_X25519 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey): return (
return self.type == 'X25519' self.type == "RSA" and self.size == self.existing_private_key.key_size
if CRYPTOGRAPHY_HAS_X448 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey): )
return self.type == 'X448' if isinstance(
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): self.existing_private_key,
return self.type == 'Ed25519' cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey,
if CRYPTOGRAPHY_HAS_ED448 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): ):
return self.type == 'Ed448' return (
if isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): self.type == "DSA" and self.size == self.existing_private_key.key_size
if self.type != 'ECC': )
if CRYPTOGRAPHY_HAS_X25519 and isinstance(
self.existing_private_key,
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey,
):
return self.type == "X25519"
if CRYPTOGRAPHY_HAS_X448 and isinstance(
self.existing_private_key,
cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey,
):
return self.type == "X448"
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(
self.existing_private_key,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey,
):
return self.type == "Ed25519"
if CRYPTOGRAPHY_HAS_ED448 and isinstance(
self.existing_private_key,
cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey,
):
return self.type == "Ed448"
if isinstance(
self.existing_private_key,
cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey,
):
if self.type != "ECC":
return False return False
if self.curve not in self.curves: if self.curve not in self.curves:
return False return False
return self.curves[self.curve]['verify'](self.existing_private_key) return self.curves[self.curve]["verify"](self.existing_private_key)
return False return False
def _check_format(self): def _check_format(self):
if self.format == 'auto_ignore': if self.format == "auto_ignore":
return True return True
try: try:
format = identify_private_key_format(self.existing_private_key_bytes) format = identify_private_key_format(self.existing_private_key_bytes)
return format == self._get_wanted_format() return format == self._get_wanted_format()
except Exception as dummy: except Exception:
return False return False
def select_backend(module, backend): def select_backend(module, backend):
if backend == 'auto': if backend == "auto":
# Detection what is possible # Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) can_use_cryptography = (
CRYPTOGRAPHY_FOUND
and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
)
# Decision # Decision
if can_use_cryptography: if can_use_cryptography:
backend = 'cryptography' backend = "cryptography"
# Success? # Success?
if backend == 'auto': if backend == "auto":
module.fail_json(msg=("Cannot detect the required Python library " module.fail_json(
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) msg=(
if backend == 'cryptography': "Cannot detect the required Python library " "cryptography (>= {0})"
).format(MINIMAL_CRYPTOGRAPHY_VERSION)
)
if backend == "cryptography":
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), module.fail_json(
exception=CRYPTOGRAPHY_IMP_ERR) msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
return backend, PrivateKeyCryptographyBackend(module) return backend, PrivateKeyCryptographyBackend(module)
else: else:
raise Exception('Unsupported value for backend: {0}'.format(backend)) raise Exception("Unsupported value for backend: {0}".format(backend))
def get_privatekey_argument_spec(): def get_privatekey_argument_spec():
return ArgumentSpec( return ArgumentSpec(
argument_spec=dict( argument_spec=dict(
size=dict(type='int', default=4096), size=dict(type="int", default=4096),
type=dict(type='str', default='RSA', choices=[ type=dict(
'DSA', 'ECC', 'Ed25519', 'Ed448', 'RSA', 'X25519', 'X448' type="str",
]), default="RSA",
curve=dict(type='str', choices=[ choices=["DSA", "ECC", "Ed25519", "Ed448", "RSA", "X25519", "X448"],
'secp224r1', 'secp256k1', 'secp256r1', 'secp384r1', 'secp521r1', ),
'secp192r1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', curve=dict(
'sect163k1', 'sect163r2', 'sect233k1', 'sect233r1', 'sect283k1', type="str",
'sect283r1', 'sect409k1', 'sect409r1', 'sect571k1', 'sect571r1', choices=[
]), "secp224r1",
passphrase=dict(type='str', no_log=True), "secp256k1",
cipher=dict(type='str', default='auto'), "secp256r1",
format=dict(type='str', default='auto_ignore', choices=['pkcs1', 'pkcs8', 'raw', 'auto', 'auto_ignore']), "secp384r1",
format_mismatch=dict(type='str', default='regenerate', choices=['regenerate', 'convert']), "secp521r1",
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'), "secp192r1",
"brainpoolP256r1",
"brainpoolP384r1",
"brainpoolP512r1",
"sect163k1",
"sect163r2",
"sect233k1",
"sect233r1",
"sect283k1",
"sect283r1",
"sect409k1",
"sect409r1",
"sect571k1",
"sect571r1",
],
),
passphrase=dict(type="str", no_log=True),
cipher=dict(type="str", default="auto"),
format=dict(
type="str",
default="auto_ignore",
choices=["pkcs1", "pkcs8", "raw", "auto", "auto_ignore"],
),
format_mismatch=dict(
type="str", default="regenerate", choices=["regenerate", "convert"]
),
select_crypto_backend=dict(
type="str", choices=["auto", "cryptography"], default="auto"
),
regenerate=dict( regenerate=dict(
type='str', type="str",
default='full_idempotence', default="full_idempotence",
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always'] choices=[
"never",
"fail",
"partial_idempotence",
"full_idempotence",
"always",
],
), ),
), ),
required_if=[ required_if=[
['type', 'ECC', ['curve']], ["type", "ECC", ["curve"]],
], ],
) )

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -14,44 +16,41 @@ import traceback
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.common.text.converters import to_bytes from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.argspec import (
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec ArgumentSpec,
from ansible_collections.community.crypto.plugins.module_utils.io import (
load_file,
) )
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
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, CRYPTOGRAPHY_HAS_ED448,
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_X448,
CRYPTOGRAPHY_HAS_X25519,
OpenSSLObjectError, OpenSSLObjectError,
) )
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_compare_private_keys, cryptography_compare_private_keys,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( 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.io import load_file
from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' MINIMAL_CRYPTOGRAPHY_VERSION = "1.2.3"
CRYPTOGRAPHY_IMP_ERR = None CRYPTOGRAPHY_IMP_ERR = None
try: try:
import cryptography import cryptography
import cryptography.exceptions import cryptography.exceptions
import cryptography.hazmat.backends import cryptography.hazmat.backends
import cryptography.hazmat.primitives.serialization
import cryptography.hazmat.primitives.asymmetric.rsa
import cryptography.hazmat.primitives.asymmetric.dsa import cryptography.hazmat.primitives.asymmetric.dsa
import cryptography.hazmat.primitives.asymmetric.ec import cryptography.hazmat.primitives.asymmetric.ec
import cryptography.hazmat.primitives.asymmetric.rsa
import cryptography.hazmat.primitives.asymmetric.utils import cryptography.hazmat.primitives.asymmetric.utils
import cryptography.hazmat.primitives.serialization
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError: except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -75,18 +74,18 @@ class PrivateKeyError(OpenSSLObjectError):
class PrivateKeyConvertBackend: class PrivateKeyConvertBackend:
def __init__(self, module, backend): def __init__(self, module, backend):
self.module = module self.module = module
self.src_path = module.params['src_path'] self.src_path = module.params["src_path"]
self.src_content = module.params['src_content'] self.src_content = module.params["src_content"]
self.src_passphrase = module.params['src_passphrase'] self.src_passphrase = module.params["src_passphrase"]
self.format = module.params['format'] self.format = module.params["format"]
self.dest_passphrase = module.params['dest_passphrase'] self.dest_passphrase = module.params["dest_passphrase"]
self.backend = backend self.backend = backend
self.src_private_key = None self.src_private_key = None
if self.src_path is not None: if self.src_path is not None:
self.src_private_key_bytes = load_file(self.src_path, module) self.src_private_key_bytes = load_file(self.src_path, module)
else: else:
self.src_private_key_bytes = self.src_content.encode('utf-8') self.src_private_key_bytes = self.src_content.encode("utf-8")
self.dest_private_key = None self.dest_private_key = None
self.dest_private_key_bytes = None self.dest_private_key_bytes = None
@@ -111,17 +110,25 @@ class PrivateKeyConvertBackend:
def needs_conversion(self): def needs_conversion(self):
"""Check whether a conversion is necessary. Must only be called if needs_regeneration() returned False.""" """Check whether a conversion is necessary. Must only be called if needs_regeneration() returned False."""
dummy, self.src_private_key = self._load_private_key(self.src_private_key_bytes, self.src_passphrase) dummy, self.src_private_key = self._load_private_key(
self.src_private_key_bytes, self.src_passphrase
)
if not self.has_existing_destination(): if not self.has_existing_destination():
return True return True
try: try:
format, self.dest_private_key = self._load_private_key(self.dest_private_key_bytes, self.dest_passphrase, current_hint=self.src_private_key) format, self.dest_private_key = self._load_private_key(
self.dest_private_key_bytes,
self.dest_passphrase,
current_hint=self.src_private_key,
)
except Exception: except Exception:
return True return True
return format != self.format or not cryptography_compare_private_keys(self.dest_private_key, self.src_private_key) return format != self.format or not cryptography_compare_private_keys(
self.dest_private_key, self.src_private_key
)
def dump(self): def dump(self):
"""Serialize the object into a dictionary.""" """Serialize the object into a dictionary."""
@@ -131,7 +138,9 @@ class PrivateKeyConvertBackend:
# Implementation with using cryptography # Implementation with using cryptography
class PrivateKeyConvertCryptographyBackend(PrivateKeyConvertBackend): class PrivateKeyConvertCryptographyBackend(PrivateKeyConvertBackend):
def __init__(self, module): def __init__(self, module):
super(PrivateKeyConvertCryptographyBackend, self).__init__(module=module, backend='cryptography') super(PrivateKeyConvertCryptographyBackend, self).__init__(
module=module, backend="cryptography"
)
self.cryptography_backend = cryptography.hazmat.backends.default_backend() self.cryptography_backend = cryptography.hazmat.backends.default_backend()
@@ -140,72 +149,140 @@ class PrivateKeyConvertCryptographyBackend(PrivateKeyConvertBackend):
# Select export format and encoding # Select export format and encoding
try: try:
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM
if self.format == 'pkcs1': if self.format == "pkcs1":
# "TraditionalOpenSSL" format is PKCS1 # "TraditionalOpenSSL" format is PKCS1
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL export_format = (
elif self.format == 'pkcs8': cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8 )
elif self.format == 'raw': elif self.format == "pkcs8":
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.Raw export_format = (
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.Raw cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
)
elif self.format == "raw":
export_format = (
cryptography.hazmat.primitives.serialization.PrivateFormat.Raw
)
export_encoding = (
cryptography.hazmat.primitives.serialization.Encoding.Raw
)
except AttributeError: except AttributeError:
self.module.fail_json(msg='Cryptography backend does not support the selected output format "{0}"'.format(self.format)) self.module.fail_json(
msg='Cryptography backend does not support the selected output format "{0}"'.format(
self.format
)
)
# Select key encryption # Select key encryption
encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption() encryption_algorithm = (
cryptography.hazmat.primitives.serialization.NoEncryption()
)
if self.dest_passphrase: if self.dest_passphrase:
encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(to_bytes(self.dest_passphrase)) encryption_algorithm = (
cryptography.hazmat.primitives.serialization.BestAvailableEncryption(
to_bytes(self.dest_passphrase)
)
)
# Serialize key # Serialize key
try: try:
return self.src_private_key.private_bytes( return self.src_private_key.private_bytes(
encoding=export_encoding, encoding=export_encoding,
format=export_format, format=export_format,
encryption_algorithm=encryption_algorithm encryption_algorithm=encryption_algorithm,
) )
except ValueError as dummy: except ValueError:
self.module.fail_json( self.module.fail_json(
msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(self.format) msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(
self.format
) )
except Exception as dummy: )
except Exception:
self.module.fail_json( self.module.fail_json(
msg='Error while serializing the private key in the required format "{0}"'.format(self.format), msg='Error while serializing the private key in the required format "{0}"'.format(
exception=traceback.format_exc() self.format
),
exception=traceback.format_exc(),
) )
def _load_private_key(self, data, passphrase, current_hint=None): def _load_private_key(self, data, passphrase, current_hint=None):
try: try:
# Interpret bytes depending on format. # Interpret bytes depending on format.
format = identify_private_key_format(data) format = identify_private_key_format(data)
if format == 'raw': if format == "raw":
if passphrase is not None: if passphrase is not None:
raise PrivateKeyError('Cannot load raw key with passphrase') raise PrivateKeyError("Cannot load raw key with passphrase")
if len(data) == 56 and CRYPTOGRAPHY_HAS_X448: if len(data) == 56 and CRYPTOGRAPHY_HAS_X448:
return format, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data) return (
format,
cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(
data
),
)
if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448: if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448:
return format, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data) return (
format,
cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(
data
),
)
if len(data) == 32: if len(data) == 32:
if CRYPTOGRAPHY_HAS_X25519 and not CRYPTOGRAPHY_HAS_ED25519: if CRYPTOGRAPHY_HAS_X25519 and not CRYPTOGRAPHY_HAS_ED25519:
return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) return (
format,
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(
data
),
)
if CRYPTOGRAPHY_HAS_ED25519 and not CRYPTOGRAPHY_HAS_X25519: if CRYPTOGRAPHY_HAS_ED25519 and not CRYPTOGRAPHY_HAS_X25519:
return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) return (
format,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(
data
),
)
if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519: if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519:
if isinstance(current_hint, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey): if isinstance(
current_hint,
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey,
):
try: try:
return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) return (
format,
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(
data
),
)
except Exception: except Exception:
return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) return (
format,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(
data
),
)
else: else:
try: try:
return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) return (
format,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(
data
),
)
except Exception: except Exception:
return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) return (
raise PrivateKeyError('Cannot load raw key') format,
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(
data
),
)
raise PrivateKeyError("Cannot load raw key")
else: else:
return format, cryptography.hazmat.primitives.serialization.load_pem_private_key( return (
format,
cryptography.hazmat.primitives.serialization.load_pem_private_key(
data, data,
None if passphrase is None else to_bytes(passphrase), None if passphrase is None else to_bytes(passphrase),
backend=self.cryptography_backend backend=self.cryptography_backend,
),
) )
except Exception as e: except Exception as e:
raise PrivateKeyError(e) raise PrivateKeyError(e)
@@ -213,24 +290,28 @@ class PrivateKeyConvertCryptographyBackend(PrivateKeyConvertBackend):
def select_backend(module): def select_backend(module):
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), module.fail_json(
exception=CRYPTOGRAPHY_IMP_ERR) msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
return PrivateKeyConvertCryptographyBackend(module) return PrivateKeyConvertCryptographyBackend(module)
def get_privatekey_argument_spec(): def get_privatekey_argument_spec():
return ArgumentSpec( return ArgumentSpec(
argument_spec=dict( argument_spec=dict(
src_path=dict(type='path'), src_path=dict(type="path"),
src_content=dict(type='str'), src_content=dict(type="str"),
src_passphrase=dict(type='str', no_log=True), src_passphrase=dict(type="str", no_log=True),
dest_passphrase=dict(type='str', no_log=True), dest_passphrase=dict(type="str", no_log=True),
format=dict(type='str', required=True, choices=['pkcs1', 'pkcs8', 'raw']), format=dict(type="str", required=True, choices=["pkcs1", "pkcs8", "raw"]),
), ),
mutually_exclusive=[ mutually_exclusive=[
['src_path', 'src_content'], ["src_path", "src_content"],
], ],
required_one_of=[ required_one_of=[
['src_path', 'src_content'], ["src_path", "src_content"],
], ],
) )

View File

@@ -7,6 +7,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -15,37 +17,35 @@ import traceback
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.common.text.converters import to_native, to_bytes from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_ED448, CRYPTOGRAPHY_HAS_ED448,
CRYPTOGRAPHY_HAS_ED25519,
OpenSSLObjectError, 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 ( from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
binary_exp_mod, binary_exp_mod,
quick_is_not_prime, quick_is_not_prime,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
_get_cryptography_public_key_info, _get_cryptography_public_key_info,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
get_fingerprint_of_bytes,
load_privatekey,
)
from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' MINIMAL_CRYPTOGRAPHY_VERSION = "1.2.3"
CRYPTOGRAPHY_IMP_ERR = None CRYPTOGRAPHY_IMP_ERR = None
try: try:
import cryptography import cryptography
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError: except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -53,7 +53,7 @@ except ImportError:
else: else:
CRYPTOGRAPHY_FOUND = True CRYPTOGRAPHY_FOUND = True
SIGNATURE_TEST_DATA = b'1234' SIGNATURE_TEST_DATA = b"1234"
def _get_cryptography_private_key_info(key, need_private_key_data=False): def _get_cryptography_private_key_info(key, need_private_key_data=False):
@@ -62,25 +62,29 @@ def _get_cryptography_private_key_info(key, need_private_key_data=False):
if need_private_key_data: if need_private_key_data:
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
private_numbers = key.private_numbers() private_numbers = key.private_numbers()
key_private_data['p'] = private_numbers.p key_private_data["p"] = private_numbers.p
key_private_data['q'] = private_numbers.q key_private_data["q"] = private_numbers.q
key_private_data['exponent'] = private_numbers.d key_private_data["exponent"] = private_numbers.d
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): elif isinstance(
key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey
):
private_numbers = key.private_numbers() private_numbers = key.private_numbers()
key_private_data['x'] = private_numbers.x key_private_data["x"] = private_numbers.x
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): elif isinstance(
key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey
):
private_numbers = key.private_numbers() private_numbers = key.private_numbers()
key_private_data['multiplier'] = private_numbers.private_value key_private_data["multiplier"] = private_numbers.private_value
return key_type, key_public_data, key_private_data return key_type, key_public_data, key_private_data
def _check_dsa_consistency(key_public_data, key_private_data): def _check_dsa_consistency(key_public_data, key_private_data):
# Get parameters # Get parameters
p = key_public_data.get('p') p = key_public_data.get("p")
q = key_public_data.get('q') q = key_public_data.get("q")
g = key_public_data.get('g') g = key_public_data.get("g")
y = key_public_data.get('y') y = key_public_data.get("y")
x = key_private_data.get('x') x = key_private_data.get("x")
for v in (p, q, g, y, x): for v in (p, q, g, y, x):
if v is None: if v is None:
return None return None
@@ -105,10 +109,12 @@ def _check_dsa_consistency(key_public_data, key_private_data):
return True return True
def _is_cryptography_key_consistent(key, key_public_data, key_private_data, warn_func=None): def _is_cryptography_key_consistent(
key, key_public_data, key_private_data, warn_func=None
):
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
# key._backend was removed in cryptography 42.0.0 # key._backend was removed in cryptography 42.0.0
backend = getattr(key, '_backend', None) backend = getattr(key, "_backend", None)
if backend is not None: if backend is not None:
return bool(backend._lib.RSA_check_key(key._rsa_cdata)) return bool(backend._lib.RSA_check_key(key._rsa_cdata))
if isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): if isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
@@ -116,7 +122,9 @@ def _is_cryptography_key_consistent(key, key_public_data, key_private_data, warn
if result is not None: if result is not None:
return result return result
try: try:
signature = key.sign(SIGNATURE_TEST_DATA, cryptography.hazmat.primitives.hashes.SHA256()) signature = key.sign(
SIGNATURE_TEST_DATA, cryptography.hazmat.primitives.hashes.SHA256()
)
except AttributeError: except AttributeError:
# sign() was added in cryptography 1.5, but we support older versions # sign() was added in cryptography 1.5, but we support older versions
return None return None
@@ -124,16 +132,20 @@ def _is_cryptography_key_consistent(key, key_public_data, key_private_data, warn
key.public_key().verify( key.public_key().verify(
signature, signature,
SIGNATURE_TEST_DATA, SIGNATURE_TEST_DATA,
cryptography.hazmat.primitives.hashes.SHA256() cryptography.hazmat.primitives.hashes.SHA256(),
) )
return True return True
except cryptography.exceptions.InvalidSignature: except cryptography.exceptions.InvalidSignature:
return False return False
if isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): if isinstance(
key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey
):
try: try:
signature = key.sign( signature = key.sign(
SIGNATURE_TEST_DATA, SIGNATURE_TEST_DATA,
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256()) cryptography.hazmat.primitives.asymmetric.ec.ECDSA(
cryptography.hazmat.primitives.hashes.SHA256()
),
) )
except AttributeError: except AttributeError:
# sign() was added in cryptography 1.5, but we support older versions # sign() was added in cryptography 1.5, but we support older versions
@@ -142,15 +154,21 @@ def _is_cryptography_key_consistent(key, key_public_data, key_private_data, warn
key.public_key().verify( key.public_key().verify(
signature, signature,
SIGNATURE_TEST_DATA, SIGNATURE_TEST_DATA,
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256()) cryptography.hazmat.primitives.asymmetric.ec.ECDSA(
cryptography.hazmat.primitives.hashes.SHA256()
),
) )
return True return True
except cryptography.exceptions.InvalidSignature: except cryptography.exceptions.InvalidSignature:
return False return False
has_simple_sign_function = False has_simple_sign_function = False
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): if CRYPTOGRAPHY_HAS_ED25519 and isinstance(
key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
):
has_simple_sign_function = True has_simple_sign_function = True
if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): if CRYPTOGRAPHY_HAS_ED448 and isinstance(
key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey
):
has_simple_sign_function = True has_simple_sign_function = True
if has_simple_sign_function: if has_simple_sign_function:
signature = key.sign(SIGNATURE_TEST_DATA) signature = key.sign(SIGNATURE_TEST_DATA)
@@ -161,7 +179,7 @@ def _is_cryptography_key_consistent(key, key_public_data, key_private_data, warn
return False return False
# For X25519 and X448, there's no test yet. # For X25519 and X448, there's no test yet.
if warn_func is not None: if warn_func is not None:
warn_func('Cannot determine consistency for key of type %s' % type(key)) warn_func("Cannot determine consistency for key of type %s" % type(key))
return None return None
@@ -181,7 +199,15 @@ class PrivateKeyParseError(OpenSSLObjectError):
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class PrivateKeyInfoRetrieval(object): class PrivateKeyInfoRetrieval(object):
def __init__(self, module, backend, content, passphrase=None, return_private_key_data=False, check_consistency=False): def __init__(
self,
module,
backend,
content,
passphrase=None,
return_private_key_data=False,
check_consistency=False,
):
# content must be a bytes string # content must be a bytes string
self.module = module self.module = module
self.backend = backend self.backend = backend
@@ -212,28 +238,38 @@ class PrivateKeyInfoRetrieval(object):
self.key = load_privatekey( self.key = load_privatekey(
path=None, path=None,
content=priv_key_detail, content=priv_key_detail,
passphrase=to_bytes(self.passphrase) if self.passphrase is not None else self.passphrase, passphrase=(
backend=self.backend to_bytes(self.passphrase)
if self.passphrase is not None
else self.passphrase
),
backend=self.backend,
) )
result['can_parse_key'] = True result["can_parse_key"] = True
except OpenSSLObjectError as exc: except OpenSSLObjectError as exc:
raise PrivateKeyParseError(to_native(exc), result) raise PrivateKeyParseError(to_native(exc), result)
result['public_key'] = to_native(self._get_public_key(binary=False)) result["public_key"] = to_native(self._get_public_key(binary=False))
pk = self._get_public_key(binary=True) pk = self._get_public_key(binary=True)
result['public_key_fingerprints'] = get_fingerprint_of_bytes( result["public_key_fingerprints"] = (
pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict() get_fingerprint_of_bytes(pk, prefer_one=prefer_one_fingerprint)
if pk is not None
else dict()
)
key_type, key_public_data, key_private_data = self._get_key_info( key_type, key_public_data, key_private_data = self._get_key_info(
need_private_key_data=self.return_private_key_data or self.check_consistency) need_private_key_data=self.return_private_key_data or self.check_consistency
result['type'] = key_type )
result['public_data'] = key_public_data result["type"] = key_type
result["public_data"] = key_public_data
if self.return_private_key_data: if self.return_private_key_data:
result['private_data'] = key_private_data result["private_data"] = key_private_data
if self.check_consistency: if self.check_consistency:
result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data) result["key_is_consistent"] = self._is_key_consistent(
if result['key_is_consistent'] is False: 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 do not know") # Only fail when it is False, to avoid to fail on None (which means "we do not know")
msg = ( msg = (
"Private key is not consistent! (See " "Private key is not consistent! (See "
@@ -245,48 +281,88 @@ class PrivateKeyInfoRetrieval(object):
class PrivateKeyInfoRetrievalCryptography(PrivateKeyInfoRetrieval): class PrivateKeyInfoRetrievalCryptography(PrivateKeyInfoRetrieval):
"""Validate the supplied private key, using the cryptography backend""" """Validate the supplied private key, using the cryptography backend"""
def __init__(self, module, content, **kwargs): def __init__(self, module, content, **kwargs):
super(PrivateKeyInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, **kwargs) super(PrivateKeyInfoRetrievalCryptography, self).__init__(
module, "cryptography", content, **kwargs
)
def _get_public_key(self, binary): def _get_public_key(self, binary):
return self.key.public_key().public_bytes( return self.key.public_key().public_bytes(
serialization.Encoding.DER if binary else serialization.Encoding.PEM, serialization.Encoding.DER if binary else serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo serialization.PublicFormat.SubjectPublicKeyInfo,
) )
def _get_key_info(self, need_private_key_data=False): def _get_key_info(self, need_private_key_data=False):
return _get_cryptography_private_key_info(self.key, need_private_key_data=need_private_key_data) return _get_cryptography_private_key_info(
self.key, need_private_key_data=need_private_key_data
)
def _is_key_consistent(self, key_public_data, key_private_data): def _is_key_consistent(self, key_public_data, key_private_data):
return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data, warn_func=self.module.warn) return _is_cryptography_key_consistent(
self.key, key_public_data, key_private_data, warn_func=self.module.warn
)
def get_privatekey_info(module, backend, content, passphrase=None, return_private_key_data=False, prefer_one_fingerprint=False): def get_privatekey_info(
if backend == 'cryptography': module,
backend,
content,
passphrase=None,
return_private_key_data=False,
prefer_one_fingerprint=False,
):
if backend == "cryptography":
info = PrivateKeyInfoRetrievalCryptography( info = PrivateKeyInfoRetrievalCryptography(
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data) module,
content,
passphrase=passphrase,
return_private_key_data=return_private_key_data,
)
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint) return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
def select_backend(module, backend, content, passphrase=None, return_private_key_data=False, check_consistency=False): def select_backend(
if backend == 'auto': module,
backend,
content,
passphrase=None,
return_private_key_data=False,
check_consistency=False,
):
if backend == "auto":
# Detection what is possible # Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) can_use_cryptography = (
CRYPTOGRAPHY_FOUND
and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
)
# Try cryptography # Try cryptography
if can_use_cryptography: if can_use_cryptography:
backend = 'cryptography' backend = "cryptography"
# Success? # Success?
if backend == 'auto': if backend == "auto":
module.fail_json(msg=("Cannot detect the required Python library " module.fail_json(
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) msg=(
"Cannot detect the required Python library " "cryptography (>= {0})"
).format(MINIMAL_CRYPTOGRAPHY_VERSION)
)
if backend == 'cryptography': if backend == "cryptography":
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), module.fail_json(
exception=CRYPTOGRAPHY_IMP_ERR) msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
return backend, PrivateKeyInfoRetrievalCryptography( return backend, PrivateKeyInfoRetrievalCryptography(
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data, check_consistency=check_consistency) module,
content,
passphrase=passphrase,
return_private_key_data=return_private_key_data,
check_consistency=check_consistency,
)
else: else:
raise ValueError('Unsupported value for backend: {0}'.format(backend)) raise ValueError("Unsupported value for backend: {0}".format(backend))

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -14,29 +16,29 @@ import traceback
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.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
CRYPTOGRAPHY_HAS_X25519,
CRYPTOGRAPHY_HAS_X448,
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_ED448, CRYPTOGRAPHY_HAS_ED448,
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_X448,
CRYPTOGRAPHY_HAS_X25519,
OpenSSLObjectError, OpenSSLObjectError,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
get_fingerprint_of_bytes, get_fingerprint_of_bytes,
load_publickey, load_publickey,
) )
from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' MINIMAL_CRYPTOGRAPHY_VERSION = "1.2.3"
CRYPTOGRAPHY_IMP_ERR = None CRYPTOGRAPHY_IMP_ERR = None
try: try:
import cryptography import cryptography
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError: except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -48,37 +50,47 @@ else:
def _get_cryptography_public_key_info(key): def _get_cryptography_public_key_info(key):
key_public_data = dict() key_public_data = dict()
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey): if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
key_type = 'RSA' key_type = "RSA"
public_numbers = key.public_numbers() public_numbers = key.public_numbers()
key_public_data['size'] = key.key_size key_public_data["size"] = key.key_size
key_public_data['modulus'] = public_numbers.n key_public_data["modulus"] = public_numbers.n
key_public_data['exponent'] = public_numbers.e key_public_data["exponent"] = public_numbers.e
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey): elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey):
key_type = 'DSA' key_type = "DSA"
parameter_numbers = key.parameters().parameter_numbers() parameter_numbers = key.parameters().parameter_numbers()
public_numbers = key.public_numbers() public_numbers = key.public_numbers()
key_public_data['size'] = key.key_size key_public_data["size"] = key.key_size
key_public_data['p'] = parameter_numbers.p key_public_data["p"] = parameter_numbers.p
key_public_data['q'] = parameter_numbers.q key_public_data["q"] = parameter_numbers.q
key_public_data['g'] = parameter_numbers.g key_public_data["g"] = parameter_numbers.g
key_public_data['y'] = public_numbers.y key_public_data["y"] = public_numbers.y
elif CRYPTOGRAPHY_HAS_X25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey): elif CRYPTOGRAPHY_HAS_X25519 and isinstance(
key_type = 'X25519' key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey
elif CRYPTOGRAPHY_HAS_X448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey): ):
key_type = 'X448' key_type = "X25519"
elif CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey): elif CRYPTOGRAPHY_HAS_X448 and isinstance(
key_type = 'Ed25519' key, cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey
elif CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey): ):
key_type = 'Ed448' key_type = "X448"
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey): elif CRYPTOGRAPHY_HAS_ED25519 and isinstance(
key_type = 'ECC' 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() public_numbers = key.public_numbers()
key_public_data['curve'] = key.curve.name key_public_data["curve"] = key.curve.name
key_public_data['x'] = public_numbers.x key_public_data["x"] = public_numbers.x
key_public_data['y'] = public_numbers.y key_public_data["y"] = public_numbers.y
key_public_data['exponent_size'] = key.curve.key_size key_public_data["exponent_size"] = key.curve.key_size
else: else:
key_type = 'unknown ({0})'.format(type(key)) key_type = "unknown ({0})".format(type(key))
return key_type, key_public_data return key_type, key_public_data
@@ -115,54 +127,75 @@ class PublicKeyInfoRetrieval(object):
raise PublicKeyParseError(to_native(e), {}) raise PublicKeyParseError(to_native(e), {})
pk = self._get_public_key(binary=True) pk = self._get_public_key(binary=True)
result['fingerprints'] = get_fingerprint_of_bytes( result["fingerprints"] = (
pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict() 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() key_type, key_public_data = self._get_key_info()
result['type'] = key_type result["type"] = key_type
result['public_data'] = key_public_data result["public_data"] = key_public_data
return result return result
class PublicKeyInfoRetrievalCryptography(PublicKeyInfoRetrieval): class PublicKeyInfoRetrievalCryptography(PublicKeyInfoRetrieval):
"""Validate the supplied public key, using the cryptography backend""" """Validate the supplied public key, using the cryptography backend"""
def __init__(self, module, content=None, key=None): def __init__(self, module, content=None, key=None):
super(PublicKeyInfoRetrievalCryptography, self).__init__(module, 'cryptography', content=content, key=key) super(PublicKeyInfoRetrievalCryptography, self).__init__(
module, "cryptography", content=content, key=key
)
def _get_public_key(self, binary): def _get_public_key(self, binary):
return self.key.public_bytes( return self.key.public_bytes(
serialization.Encoding.DER if binary else serialization.Encoding.PEM, serialization.Encoding.DER if binary else serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo serialization.PublicFormat.SubjectPublicKeyInfo,
) )
def _get_key_info(self): def _get_key_info(self):
return _get_cryptography_public_key_info(self.key) return _get_cryptography_public_key_info(self.key)
def get_publickey_info(module, backend, content=None, key=None, prefer_one_fingerprint=False): def get_publickey_info(
if backend == 'cryptography': module, backend, content=None, key=None, prefer_one_fingerprint=False
):
if backend == "cryptography":
info = PublicKeyInfoRetrievalCryptography(module, content=content, key=key) info = PublicKeyInfoRetrievalCryptography(module, content=content, key=key)
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint) return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
def select_backend(module, backend, content=None, key=None): def select_backend(module, backend, content=None, key=None):
if backend == 'auto': if backend == "auto":
# Detection what is possible # Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) can_use_cryptography = (
CRYPTOGRAPHY_FOUND
and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
)
# Try cryptography # Try cryptography
if can_use_cryptography: if can_use_cryptography:
backend = 'cryptography' backend = "cryptography"
# Success? # Success?
if backend == 'auto': if backend == "auto":
module.fail_json(msg=("Cannot detect any of the required Python libraries " module.fail_json(
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION)) msg=(
"Cannot detect any of the required Python libraries "
"cryptography (>= {0})"
).format(MINIMAL_CRYPTOGRAPHY_VERSION)
)
if backend == 'cryptography': if backend == "cryptography":
if not CRYPTOGRAPHY_FOUND: if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), module.fail_json(
exception=CRYPTOGRAPHY_IMP_ERR) msg=missing_required_lib(
return backend, PublicKeyInfoRetrievalCryptography(module, content=content, key=key) "cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
return backend, PublicKeyInfoRetrievalCryptography(
module, content=content, key=key
)
else: else:
raise ValueError('Unsupported value for backend: {0}'.format(backend)) raise ValueError("Unsupported value for backend: {0}".format(backend))

View File

@@ -5,9 +5,11 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
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 # This import is only to maintain backwards compatibility
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import ( # noqa: F401, pylint: disable=unused-import from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import ( # noqa: F401, pylint: disable=unused-import
parse_openssh_version parse_openssh_version,
) )

View File

@@ -5,32 +5,38 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
PEM_START = '-----BEGIN ' PEM_START = "-----BEGIN "
PEM_END_START = '-----END ' PEM_END_START = "-----END "
PEM_END = '-----' PEM_END = "-----"
PKCS8_PRIVATEKEY_NAMES = ('PRIVATE KEY', 'ENCRYPTED PRIVATE KEY') PKCS8_PRIVATEKEY_NAMES = ("PRIVATE KEY", "ENCRYPTED PRIVATE KEY")
PKCS1_PRIVATEKEY_SUFFIX = ' PRIVATE KEY' PKCS1_PRIVATEKEY_SUFFIX = " PRIVATE KEY"
def identify_pem_format(content, encoding='utf-8'): def identify_pem_format(content, encoding="utf-8"):
'''Given the contents of a binary file, tests whether this could be a PEM file.''' """Given the contents of a binary file, tests whether this could be a PEM file."""
try: try:
first_pem = extract_first_pem(content.decode(encoding)) first_pem = extract_first_pem(content.decode(encoding))
if first_pem is None: if first_pem is None:
return False return False
lines = first_pem.splitlines(False) lines = first_pem.splitlines(False)
if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END): if (
lines[0].startswith(PEM_START)
and lines[0].endswith(PEM_END)
and len(lines[0]) > len(PEM_START) + len(PEM_END)
):
return True return True
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
return False return False
def identify_private_key_format(content, encoding='utf-8'): def identify_private_key_format(content, encoding="utf-8"):
'''Given the contents of a private key file, identifies its format.''' """Given the contents of a private key file, identifies its format."""
# See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85 # See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85
# (PEM_read_bio_PrivateKey) # (PEM_read_bio_PrivateKey)
# and https://github.com/openssl/openssl/blob/master/include/openssl/pem.h#L46-L47 # and https://github.com/openssl/openssl/blob/master/include/openssl/pem.h#L46-L47
@@ -38,42 +44,48 @@ def identify_private_key_format(content, encoding='utf-8'):
try: try:
first_pem = extract_first_pem(content.decode(encoding)) first_pem = extract_first_pem(content.decode(encoding))
if first_pem is None: if first_pem is None:
return 'raw' return "raw"
lines = first_pem.splitlines(False) lines = first_pem.splitlines(False)
if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END): if (
name = lines[0][len(PEM_START):-len(PEM_END)] lines[0].startswith(PEM_START)
and lines[0].endswith(PEM_END)
and len(lines[0]) > len(PEM_START) + len(PEM_END)
):
name = lines[0][len(PEM_START) : -len(PEM_END)]
if name in PKCS8_PRIVATEKEY_NAMES: if name in PKCS8_PRIVATEKEY_NAMES:
return 'pkcs8' return "pkcs8"
if len(name) > len(PKCS1_PRIVATEKEY_SUFFIX) and name.endswith(PKCS1_PRIVATEKEY_SUFFIX): if len(name) > len(PKCS1_PRIVATEKEY_SUFFIX) and name.endswith(
return 'pkcs1' PKCS1_PRIVATEKEY_SUFFIX
return 'unknown-pem' ):
return "pkcs1"
return "unknown-pem"
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
return 'raw' return "raw"
def split_pem_list(text, keep_inbetween=False): def split_pem_list(text, keep_inbetween=False):
''' """
Split concatenated PEM objects into a list of strings, where each is one PEM object. Split concatenated PEM objects into a list of strings, where each is one PEM object.
''' """
result = [] result = []
current = [] if keep_inbetween else None current = [] if keep_inbetween else None
for line in text.splitlines(True): for line in text.splitlines(True):
if line.strip(): if line.strip():
if not keep_inbetween and line.startswith('-----BEGIN '): if not keep_inbetween and line.startswith("-----BEGIN "):
current = [] current = []
if current is not None: if current is not None:
current.append(line) current.append(line)
if line.startswith('-----END '): if line.startswith("-----END "):
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): def extract_first_pem(text):
''' """
Given one PEM or multiple concatenated PEM objects, return only the first one, or None if there is none. 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) all_pems = split_pem_list(text)
if not all_pems: if not all_pems:
return None return None
@@ -85,24 +97,42 @@ def _extract_type(line, start=PEM_START):
return None return None
if not line.endswith(PEM_END): if not line.endswith(PEM_END):
return None return None
return line[len(start):-len(PEM_END)] return line[len(start) : -len(PEM_END)]
def extract_pem(content, strict=False): def extract_pem(content, strict=False):
lines = content.splitlines() lines = content.splitlines()
if len(lines) < 3: if len(lines) < 3:
raise ValueError('PEM must have at least 3 lines, have only {count}'.format(count=len(lines))) raise ValueError(
"PEM must have at least 3 lines, have only {count}".format(count=len(lines))
)
header_type = _extract_type(lines[0]) header_type = _extract_type(lines[0])
if header_type is None: if header_type is None:
raise ValueError('First line is not of format {start}...{end}: {line!r}'.format(start=PEM_START, end=PEM_END, line=lines[0])) raise ValueError(
"First line is not of format {start}...{end}: {line!r}".format(
start=PEM_START, end=PEM_END, line=lines[0]
)
)
footer_type = _extract_type(lines[-1], start=PEM_END_START) footer_type = _extract_type(lines[-1], start=PEM_END_START)
if strict: if strict:
if header_type != footer_type: if header_type != footer_type:
raise ValueError('Header type ({header}) is different from footer type ({footer})'.format(header=header_type, footer=footer_type)) raise ValueError(
"Header type ({header}) is different from footer type ({footer})".format(
header=header_type, footer=footer_type
)
)
for idx, line in enumerate(lines[1:-2]): for idx, line in enumerate(lines[1:-2]):
if len(line) != 64: if len(line) != 64:
raise ValueError('Line {idx} has length {len} instead of 64'.format(idx=idx, len=len(line))) raise ValueError(
"Line {idx} has length {len} instead of 64".format(
idx=idx, len=len(line)
)
)
if not (0 < len(lines[-2]) <= 64): if not (0 < len(lines[-2]) <= 64):
raise ValueError('Last line has length {len}, should be in (0, 64]'.format(len=len(lines[-2]))) raise ValueError(
"Last line has length {len}, should be in (0, 64]".format(
len=len(lines[-2])
)
)
content = lines[1:-1] content = lines[1:-1]
return header_type, ''.join(content) return header_type, "".join(content)

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -15,21 +17,22 @@ import os
from ansible.module_utils import six from ansible.module_utils import six
from ansible.module_utils.common.text.converters 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 (
identify_pem_format, identify_pem_format,
) )
# These imports are for backwards compatibility:
from ansible_collections.community.crypto.plugins.module_utils.time import ( # noqa: F401, pylint: disable=unused-import from ansible_collections.community.crypto.plugins.module_utils.time import ( # noqa: F401, pylint: disable=unused-import
# These imports are for backwards compatibility
get_now_datetime,
ensure_utc_timezone,
convert_relative_to_datetime, convert_relative_to_datetime,
ensure_utc_timezone,
get_now_datetime,
get_relative_time_option, get_relative_time_option,
) )
try: try:
from OpenSSL import crypto from OpenSSL import crypto
HAS_PYOPENSSL = True HAS_PYOPENSSL = True
except (ImportError, AttributeError): except (ImportError, AttributeError):
# Error handled in the calling module. # Error handled in the calling module.
@@ -37,24 +40,28 @@ except (ImportError, AttributeError):
try: try:
from cryptography import x509 from cryptography import x509
from cryptography.exceptions import UnsupportedAlgorithm
from cryptography.hazmat.backends import default_backend as cryptography_backend from cryptography.hazmat.backends import default_backend as cryptography_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
except ImportError: except ImportError:
# Error handled in the calling module. # Error handled in the calling module.
pass pass
from .basic import ( from .basic import OpenSSLBadPassphraseError, OpenSSLObjectError
OpenSSLObjectError,
OpenSSLBadPassphraseError,
)
# This list of preferred fingerprints is used when prefer_one=True is supplied to the # This list of preferred fingerprints is used when prefer_one=True is supplied to the
# fingerprinting methods. # fingerprinting methods.
PREFERRED_FINGERPRINTS = ( PREFERRED_FINGERPRINTS = (
'sha256', 'sha3_256', 'sha512', 'sha3_512', 'sha384', 'sha3_384', 'sha1', 'md5' "sha256",
"sha3_256",
"sha512",
"sha3_512",
"sha384",
"sha3_384",
"sha1",
"md5",
) )
@@ -73,8 +80,16 @@ def get_fingerprint_of_bytes(source, prefer_one=False):
if prefer_one: if prefer_one:
# Sort algorithms to have the ones in PREFERRED_FINGERPRINTS at the beginning # 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 = [
prefered_algorithms += sorted([algorithm for algorithm in algorithms if algorithm not in PREFERRED_FINGERPRINTS]) 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 algorithms = prefered_algorithms
for algo in algorithms: for algo in algorithms:
@@ -90,34 +105,47 @@ def get_fingerprint_of_bytes(source, prefer_one=False):
pubkey_digest = h.hexdigest() pubkey_digest = h.hexdigest()
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: if prefer_one:
break break
return fingerprint return fingerprint
def get_fingerprint_of_privatekey(privatekey, backend='cryptography', prefer_one=False): def get_fingerprint_of_privatekey(privatekey, backend="cryptography", prefer_one=False):
"""Generate the fingerprint of the public key. """ """Generate the fingerprint of the public key."""
if backend == 'cryptography': if backend == "cryptography":
publickey = privatekey.public_key().public_bytes( publickey = privatekey.public_key().public_bytes(
serialization.Encoding.DER, serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo
serialization.PublicFormat.SubjectPublicKeyInfo
) )
return get_fingerprint_of_bytes(publickey, prefer_one=prefer_one) return get_fingerprint_of_bytes(publickey, prefer_one=prefer_one)
def get_fingerprint(path, passphrase=None, content=None, backend='cryptography', prefer_one=False): def get_fingerprint(
"""Generate the fingerprint of the public key. """ path, passphrase=None, content=None, backend="cryptography", prefer_one=False
):
"""Generate the fingerprint of the public key."""
privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend) privatekey = load_privatekey(
path,
passphrase=passphrase,
content=content,
check_passphrase=False,
backend=backend,
)
return get_fingerprint_of_privatekey(privatekey, backend=backend, prefer_one=prefer_one) return get_fingerprint_of_privatekey(
privatekey, backend=backend, prefer_one=prefer_one
)
def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='cryptography'): def load_privatekey(
path, passphrase=None, check_passphrase=True, content=None, backend="cryptography"
):
"""Load the specified OpenSSL private key. """Load the specified OpenSSL private key.
The content can also be specified via content; in that case, The content can also be specified via content; in that case,
@@ -126,58 +154,76 @@ def load_privatekey(path, passphrase=None, check_passphrase=True, content=None,
try: try:
if content is None: if content is None:
with open(path, 'rb') as b_priv_key_fh: with open(path, "rb") as b_priv_key_fh:
priv_key_detail = b_priv_key_fh.read() priv_key_detail = b_priv_key_fh.read()
else: else:
priv_key_detail = content priv_key_detail = content
except (IOError, OSError) as exc: except (IOError, OSError) as exc:
raise OpenSSLObjectError(exc) raise OpenSSLObjectError(exc)
if backend == 'pyopenssl': if backend == "pyopenssl":
# First try: try to load with real passphrase (resp. empty string) # First try: try to load with real passphrase (resp. empty string)
# Will work if this is the correct passphrase, or the key is not # Will work if this is the correct passphrase, or the key is not
# password-protected. # password-protected.
try: try:
result = crypto.load_privatekey(crypto.FILETYPE_PEM, result = crypto.load_privatekey(
priv_key_detail, crypto.FILETYPE_PEM, priv_key_detail, to_bytes(passphrase or "")
to_bytes(passphrase or '')) )
except crypto.Error as e: except crypto.Error as e:
if len(e.args) > 0 and len(e.args[0]) > 0: if len(e.args) > 0 and len(e.args[0]) > 0:
if e.args[0][0][2] in ('bad decrypt', 'bad password read'): if e.args[0][0][2] in ("bad decrypt", "bad password read"):
# This happens in case we have the wrong passphrase. # This happens in case we have the wrong passphrase.
if passphrase is not None: if passphrase is not None:
raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key!') raise OpenSSLBadPassphraseError(
"Wrong passphrase provided for private key!"
)
else: else:
raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!') raise OpenSSLBadPassphraseError(
raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e)) "No passphrase provided, but private key is password-protected!"
)
raise OpenSSLObjectError("Error while deserializing key: {0}".format(e))
if check_passphrase: if check_passphrase:
# Next we want to make sure that the key is actually protected by # Next we want to make sure that the key is actually protected by
# a passphrase (in case we did try the empty string before, make # a passphrase (in case we did try the empty string before, make
# sure that the key is not protected by the empty string) # sure that the key is not protected by the empty string)
try: try:
crypto.load_privatekey(crypto.FILETYPE_PEM, crypto.load_privatekey(
crypto.FILETYPE_PEM,
priv_key_detail, priv_key_detail,
to_bytes('y' if passphrase == 'x' else 'x')) to_bytes("y" if passphrase == "x" else "x"),
)
if passphrase is not None: if passphrase is not None:
# Since we can load the key without an exception, the # Since we can load the key without an exception, the
# key is not password-protected # key is not password-protected
raise OpenSSLBadPassphraseError('Passphrase provided, but private key is not password-protected!') raise OpenSSLBadPassphraseError(
"Passphrase provided, but private key is not password-protected!"
)
except crypto.Error as e: except crypto.Error as e:
if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0: if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0:
if e.args[0][0][2] in ('bad decrypt', 'bad password read'): if e.args[0][0][2] in ("bad decrypt", "bad password read"):
# The key is obviously protected by the empty string. # The key is obviously protected by the empty string.
# Do not do this at home (if it is possible at all)... # Do not do this at home (if it is possible at all)...
raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!') raise OpenSSLBadPassphraseError(
elif backend == 'cryptography': "No passphrase provided, but private key is password-protected!"
)
elif backend == "cryptography":
try: try:
result = load_pem_private_key(priv_key_detail, result = load_pem_private_key(
priv_key_detail,
None if passphrase is None else to_bytes(passphrase), None if passphrase is None else to_bytes(passphrase),
cryptography_backend()) cryptography_backend(),
)
except UnsupportedAlgorithm as exc:
raise OpenSSLBadPassphraseError("Unsupported private key type: {exc}".format(exc=exc))
except TypeError: except TypeError:
raise OpenSSLBadPassphraseError('Wrong or empty passphrase provided for private key') raise OpenSSLBadPassphraseError(
except ValueError: "Wrong or empty passphrase provided for private key"
raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key') )
except ValueError as exc:
raise OpenSSLBadPassphraseError(
"Wrong passphrase provided for private key, or private key cannot be parsed: {exc}".format(exc=exc)
)
return result return result
@@ -185,60 +231,72 @@ def load_privatekey(path, passphrase=None, check_passphrase=True, content=None,
def load_publickey(path=None, content=None, backend=None): def load_publickey(path=None, content=None, backend=None):
if content is None: if content is None:
if path is None: if path is None:
raise OpenSSLObjectError('Must provide either path or content') raise OpenSSLObjectError("Must provide either path or content")
try: try:
with open(path, 'rb') as b_priv_key_fh: with open(path, "rb") as b_priv_key_fh:
content = b_priv_key_fh.read() content = b_priv_key_fh.read()
except (IOError, OSError) as exc: except (IOError, OSError) as exc:
raise OpenSSLObjectError(exc) raise OpenSSLObjectError(exc)
if backend == 'cryptography': if backend == "cryptography":
try: try:
return serialization.load_pem_public_key(content, backend=cryptography_backend()) return serialization.load_pem_public_key(
content, backend=cryptography_backend()
)
except Exception as e: except Exception as e:
raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e)) raise OpenSSLObjectError("Error while deserializing key: {0}".format(e))
def load_certificate(path, content=None, backend='cryptography', der_support_enabled=False): def load_certificate(
path, content=None, backend="cryptography", der_support_enabled=False
):
"""Load the specified certificate.""" """Load the specified certificate."""
try: try:
if content is None: if content is None:
with open(path, 'rb') as cert_fh: with open(path, "rb") as cert_fh:
cert_content = cert_fh.read() cert_content = cert_fh.read()
else: else:
cert_content = content cert_content = content
except (IOError, OSError) as exc: except (IOError, OSError) as exc:
raise OpenSSLObjectError(exc) raise OpenSSLObjectError(exc)
if backend == 'pyopenssl': if backend == "pyopenssl":
if der_support_enabled is False or identify_pem_format(cert_content): if der_support_enabled is False or identify_pem_format(cert_content):
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_content) return crypto.load_certificate(crypto.FILETYPE_PEM, cert_content)
elif der_support_enabled: elif der_support_enabled:
raise OpenSSLObjectError('Certificate in DER format is not supported by the pyopenssl backend.') raise OpenSSLObjectError(
elif backend == 'cryptography': "Certificate in DER format is not supported by the pyopenssl backend."
)
elif backend == "cryptography":
if der_support_enabled is False or identify_pem_format(cert_content): if der_support_enabled is False or identify_pem_format(cert_content):
try: try:
return x509.load_pem_x509_certificate(cert_content, cryptography_backend()) return x509.load_pem_x509_certificate(
cert_content, cryptography_backend()
)
except ValueError as exc: except ValueError as exc:
raise OpenSSLObjectError(exc) raise OpenSSLObjectError(exc)
elif der_support_enabled: elif der_support_enabled:
try: try:
return x509.load_der_x509_certificate(cert_content, cryptography_backend()) return x509.load_der_x509_certificate(
cert_content, cryptography_backend()
)
except ValueError as exc: except ValueError as exc:
raise OpenSSLObjectError('Cannot parse DER certificate: {0}'.format(exc)) raise OpenSSLObjectError(
"Cannot parse DER certificate: {0}".format(exc)
)
def load_certificate_request(path, content=None, backend='cryptography'): def load_certificate_request(path, content=None, backend="cryptography"):
"""Load the specified certificate signing request.""" """Load the specified certificate signing request."""
try: try:
if content is None: if content is None:
with open(path, 'rb') as csr_fh: with open(path, "rb") as csr_fh:
csr_content = csr_fh.read() csr_content = csr_fh.read()
else: else:
csr_content = content csr_content = content
except (IOError, OSError) as exc: except (IOError, OSError) as exc:
raise OpenSSLObjectError(exc) raise OpenSSLObjectError(exc)
if backend == 'cryptography': if backend == "cryptography":
try: try:
return x509.load_pem_x509_csr(csr_content, cryptography_backend()) return x509.load_pem_x509_csr(csr_content, cryptography_backend())
except ValueError as exc: except ValueError as exc:
@@ -247,23 +305,40 @@ def load_certificate_request(path, content=None, backend='cryptography'):
def parse_name_field(input_dict, name_field_name=None): def parse_name_field(input_dict, name_field_name=None):
"""Take a dict with key: value or key: list_of_values mappings and return a list of tuples""" """Take a dict with key: value or key: list_of_values mappings and return a list of tuples"""
error_str = '{key}' if name_field_name is None else '{key} in {name}' error_str = "{key}" if name_field_name is None else "{key} in {name}"
result = [] result = []
for key, value in input_dict.items(): for key, value in input_dict.items():
if isinstance(value, list): if isinstance(value, list):
for entry in value: for entry in value:
if not isinstance(entry, six.string_types): if not isinstance(entry, six.string_types):
raise TypeError(('Values %s must be strings' % error_str).format(key=key, name=name_field_name)) raise TypeError(
("Values %s must be strings" % error_str).format(
key=key, name=name_field_name
)
)
if not entry: if not entry:
raise ValueError(('Values for %s must not be empty strings' % error_str).format(key=key)) raise ValueError(
("Values for %s must not be empty strings" % error_str).format(
key=key
)
)
result.append((key, entry)) result.append((key, entry))
elif isinstance(value, six.string_types): elif isinstance(value, six.string_types):
if not value: if not value:
raise ValueError(('Value for %s must not be an empty string' % error_str).format(key=key)) raise ValueError(
("Value for %s must not be an empty string" % error_str).format(
key=key
)
)
result.append((key, value)) result.append((key, value))
else: else:
raise TypeError(('Value for %s must be either a string or a list of strings' % error_str).format(key=key)) raise TypeError(
(
"Value for %s must be either a string or a list of strings"
% error_str
).format(key=key)
)
return result return result
@@ -274,28 +349,32 @@ def parse_ordered_name_field(input_list, name_field_name):
for index, entry in enumerate(input_list): for index, entry in enumerate(input_list):
if len(entry) != 1: if len(entry) != 1:
raise ValueError( raise ValueError(
'Entry #{index} in {name} must be a dictionary with exactly one key-value pair'.format( "Entry #{index} in {name} must be a dictionary with exactly one key-value pair".format(
name=name_field_name, index=index + 1)) name=name_field_name, index=index + 1
)
)
try: try:
result.extend(parse_name_field(entry, name_field_name=name_field_name)) result.extend(parse_name_field(entry, name_field_name=name_field_name))
except (TypeError, ValueError) as exc: except (TypeError, ValueError) as exc:
raise ValueError( raise ValueError(
'Error while processing entry #{index} in {name}: {error}'.format( "Error while processing entry #{index} in {name}: {error}".format(
name=name_field_name, index=index + 1, error=exc)) name=name_field_name, index=index + 1, error=exc
)
)
return result return result
def select_message_digest(digest_string): def select_message_digest(digest_string):
digest = None digest = None
if digest_string == 'sha256': if digest_string == "sha256":
digest = hashes.SHA256() digest = hashes.SHA256()
elif digest_string == 'sha384': elif digest_string == "sha384":
digest = hashes.SHA384() digest = hashes.SHA384()
elif digest_string == 'sha512': elif digest_string == "sha512":
digest = hashes.SHA512() digest = hashes.SHA512()
elif digest_string == 'sha1': elif digest_string == "sha1":
digest = hashes.SHA1() digest = hashes.SHA1()
elif digest_string == 'md5': elif digest_string == "md5":
digest = hashes.MD5() digest = hashes.MD5()
return digest return digest
@@ -319,7 +398,7 @@ 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']): if module.check_file_absent_if_check_mode(file_args["path"]):
return False return False
return not module.set_fs_attributes_if_different(file_args, False) return not module.set_fs_attributes_if_different(file_args, False)

View File

@@ -12,6 +12,7 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import json import json
@@ -19,12 +20,13 @@ import os
import re import re
import traceback import traceback
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.common.text.converters import to_native, to_text
from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.six.moves.urllib.error import HTTPError
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.urls import Request from ansible.module_utils.urls import Request
YAML_IMP_ERR = None YAML_IMP_ERR = None
try: try:
import yaml import yaml
@@ -39,22 +41,25 @@ valid_file_format = re.compile(r".*(\.)(yml|yaml|json)$")
def ecs_client_argument_spec(): def ecs_client_argument_spec():
return dict( return dict(
entrust_api_user=dict(type='str', required=True), entrust_api_user=dict(type="str", required=True),
entrust_api_key=dict(type='str', required=True, no_log=True), entrust_api_key=dict(type="str", required=True, no_log=True),
entrust_api_client_cert_path=dict(type='path', required=True), entrust_api_client_cert_path=dict(type="path", required=True),
entrust_api_client_cert_key_path=dict(type='path', required=True, no_log=True), entrust_api_client_cert_key_path=dict(type="path", required=True, no_log=True),
entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'), entrust_api_specification_path=dict(
type="path",
default="https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml",
),
) )
class SessionConfigurationException(Exception): class SessionConfigurationException(Exception):
""" Raised if we cannot configure a session with the API """ """Raised if we cannot configure a session with the API"""
pass pass
class RestOperationException(Exception): class RestOperationException(Exception):
""" Encapsulate a REST API error """ """Encapsulate a REST API error"""
def __init__(self, error): def __init__(self, error):
self.status = to_native(error.get("status", None)) self.status = to_native(error.get("status", None))
@@ -104,7 +109,12 @@ class RestOperation(object):
self.parameters = {} self.parameters = {}
else: else:
self.parameters = parameters self.parameters = parameters
self.url = "{scheme}://{host}{base_path}{uri}".format(scheme="https", host=session._spec.get("host"), base_path=session._spec.get("basePath"), uri=uri) self.url = "{scheme}://{host}{base_path}{uri}".format(
scheme="https",
host=session._spec.get("host"),
base_path=session._spec.get("basePath"),
uri=uri,
)
def restmethod(self, *args, **kwargs): def restmethod(self, *args, **kwargs):
"""Do the hard work of making the request here""" """Do the hard work of making the request here"""
@@ -143,14 +153,14 @@ class RestOperation(object):
try: try:
if body_parameters: if body_parameters:
body_parameters_json = json.dumps(body_parameters) body_parameters_json = json.dumps(body_parameters)
response = self.session.request.open(method=self.method, url=url, data=body_parameters_json) response = self.session.request.open(
method=self.method, url=url, data=body_parameters_json
)
else: else:
response = self.session.request.open(method=self.method, url=url) response = self.session.request.open(method=self.method, url=url)
request_error = False
except HTTPError as e: except HTTPError as e:
# An HTTPError has the same methods available as a valid response from request.open # An HTTPError has the same methods available as a valid response from request.open
response = e response = e
request_error = True
# Return the result if JSON and success ({} for empty responses) # Return the result if JSON and success ({} for empty responses)
# Raise an exception if there was a failure. # Raise an exception if there was a failure.
@@ -167,11 +177,13 @@ class RestOperation(object):
raise RestOperationException(result) raise RestOperationException(result)
# Raise a generic RestOperationException if this fails # Raise a generic RestOperationException if this fails
raise RestOperationException({"status": result_code, "errors": [{"message": "REST Operation Failed"}]}) raise RestOperationException(
{"status": result_code, "errors": [{"message": "REST Operation Failed"}]}
)
class Resource(object): class Resource(object):
""" Implement basic CRUD operations against a path. """ """Implement basic CRUD operations against a path."""
def __init__(self, session): def __init__(self, session):
self.session = session self.session = session
@@ -196,13 +208,20 @@ class Resource(object):
elif method.lower() == "patch": elif method.lower() == "patch":
operation_name = "Patch" operation_name = "Patch"
else: else:
raise SessionConfigurationException(to_native("Invalid REST method type {0}".format(method))) raise SessionConfigurationException(
to_native("Invalid REST method type {0}".format(method))
)
# Get the non-parameter parts of the URL and append to the operation name # Get the non-parameter parts of the URL and append to the operation name
# e.g /application/version -> GetApplicationVersion # e.g /application/version -> GetApplicationVersion
# e.g. /application/{id} -> GetApplication # e.g. /application/{id} -> GetApplication
# This may lead to duplicates, which we must prevent. # This may lead to duplicates, which we must prevent.
operation_name += re.sub(r"{(.*)}", "", url).replace("/", " ").title().replace(" ", "") operation_name += (
re.sub(r"{(.*)}", "", url)
.replace("/", " ")
.title()
.replace(" ", "")
)
operation_spec["operationId"] = operation_name operation_spec["operationId"] = operation_name
op = RestOperation(session, url, method, parameters) op = RestOperation(session, url, method, parameters)
@@ -244,7 +263,9 @@ class ECSSession(object):
self.request.url_username = entrust_api_user self.request.url_username = entrust_api_user
self.request.url_password = entrust_api_key self.request.url_password = entrust_api_key
else: else:
raise SessionConfigurationException(to_native("User and key must be provided.")) raise SessionConfigurationException(
to_native("User and key must be provided.")
)
# set up client certificate if passed (support all-in one or cert + key) # set up client certificate if passed (support all-in one or cert + key)
entrust_api_cert = self.get_config("entrust_api_cert") entrust_api_cert = self.get_config("entrust_api_cert")
@@ -254,45 +275,78 @@ class ECSSession(object):
if entrust_api_cert_key: if entrust_api_cert_key:
self.request.client_key = entrust_api_cert_key self.request.client_key = entrust_api_cert_key
else: else:
raise SessionConfigurationException(to_native("Client certificate for authentication to the API must be provided.")) raise SessionConfigurationException(
to_native(
"Client certificate for authentication to the API must be provided."
)
)
# set up the spec # set up the spec
entrust_api_specification_path = self.get_config("entrust_api_specification_path") entrust_api_specification_path = self.get_config(
"entrust_api_specification_path"
)
if not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path): if not entrust_api_specification_path.startswith("http") and not os.path.isfile(
raise SessionConfigurationException(to_native("OpenAPI specification was not found at location {0}.".format(entrust_api_specification_path))) entrust_api_specification_path
):
raise SessionConfigurationException(
to_native(
"OpenAPI specification was not found at location {0}.".format(
entrust_api_specification_path
)
)
)
if not valid_file_format.match(entrust_api_specification_path): if not valid_file_format.match(entrust_api_specification_path):
raise SessionConfigurationException(to_native("OpenAPI specification filename must end in .json, .yml or .yaml")) raise SessionConfigurationException(
to_native(
"OpenAPI specification filename must end in .json, .yml or .yaml"
)
)
self.verify = True self.verify = True
if entrust_api_specification_path.startswith("http"): if entrust_api_specification_path.startswith("http"):
try: try:
http_response = Request().open(method="GET", url=entrust_api_specification_path) http_response = Request().open(
method="GET", url=entrust_api_specification_path
)
http_response_contents = http_response.read() http_response_contents = http_response.read()
if entrust_api_specification_path.endswith(".json"): if entrust_api_specification_path.endswith(".json"):
self._spec = json.load(http_response_contents) self._spec = json.load(http_response_contents)
elif entrust_api_specification_path.endswith(".yml") or entrust_api_specification_path.endswith(".yaml"): elif entrust_api_specification_path.endswith(
".yml"
) or entrust_api_specification_path.endswith(".yaml"):
self._spec = yaml.safe_load(http_response_contents) self._spec = yaml.safe_load(http_response_contents)
except HTTPError as e: except HTTPError as e:
raise SessionConfigurationException(to_native("Error downloading specification from address '{0}', received error code '{1}'".format( raise SessionConfigurationException(
entrust_api_specification_path, e.getcode()))) to_native(
"Error downloading specification from address '{0}', received error code '{1}'".format(
entrust_api_specification_path, e.getcode()
)
)
)
else: else:
with open(entrust_api_specification_path) as f: with open(entrust_api_specification_path) as f:
if ".json" in entrust_api_specification_path: if ".json" in entrust_api_specification_path:
self._spec = json.load(f) self._spec = json.load(f)
elif ".yml" in entrust_api_specification_path or ".yaml" in entrust_api_specification_path: elif (
".yml" in entrust_api_specification_path
or ".yaml" in entrust_api_specification_path
):
self._spec = yaml.safe_load(f) self._spec = yaml.safe_load(f)
def get_config(self, item): def get_config(self, item):
return self._config.get(item, None) return self._config.get(item, None)
def _read_config_vars(self, name, **kwargs): def _read_config_vars(self, name, **kwargs):
""" Read configuration from variables passed to the module. """ """Read configuration from variables passed to the module."""
config = {} config = {}
entrust_api_specification_path = kwargs.get("entrust_api_specification_path") entrust_api_specification_path = kwargs.get("entrust_api_specification_path")
if not entrust_api_specification_path or (not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path)): if not entrust_api_specification_path or (
not entrust_api_specification_path.startswith("http")
and not os.path.isfile(entrust_api_specification_path)
):
raise SessionConfigurationException( raise SessionConfigurationException(
to_native( to_native(
"Parameter provided for entrust_api_specification_path of value '{0}' was not a valid file path or HTTPS address.".format( "Parameter provided for entrust_api_specification_path of value '{0}' was not a valid file path or HTTPS address.".format(
@@ -305,30 +359,50 @@ class ECSSession(object):
file_path = kwargs.get(required_file) file_path = kwargs.get(required_file)
if not file_path or not os.path.isfile(file_path): if not file_path or not os.path.isfile(file_path):
raise SessionConfigurationException( raise SessionConfigurationException(
to_native("Parameter provided for {0} of value '{1}' was not a valid file path.".format(required_file, file_path)) to_native(
"Parameter provided for {0} of value '{1}' was not a valid file path.".format(
required_file, file_path
)
)
) )
for required_var in ["entrust_api_user", "entrust_api_key"]: for required_var in ["entrust_api_user", "entrust_api_key"]:
if not kwargs.get(required_var): if not kwargs.get(required_var):
raise SessionConfigurationException(to_native("Parameter provided for {0} was missing.".format(required_var))) raise SessionConfigurationException(
to_native(
"Parameter provided for {0} was missing.".format(required_var)
)
)
config["entrust_api_cert"] = kwargs.get("entrust_api_cert") config["entrust_api_cert"] = kwargs.get("entrust_api_cert")
config["entrust_api_cert_key"] = kwargs.get("entrust_api_cert_key") config["entrust_api_cert_key"] = kwargs.get("entrust_api_cert_key")
config["entrust_api_specification_path"] = kwargs.get("entrust_api_specification_path") config["entrust_api_specification_path"] = kwargs.get(
"entrust_api_specification_path"
)
config["entrust_api_user"] = kwargs.get("entrust_api_user") config["entrust_api_user"] = kwargs.get("entrust_api_user")
config["entrust_api_key"] = kwargs.get("entrust_api_key") config["entrust_api_key"] = kwargs.get("entrust_api_key")
return config return config
def ECSClient(entrust_api_user=None, entrust_api_key=None, entrust_api_cert=None, entrust_api_cert_key=None, entrust_api_specification_path=None): def ECSClient(
entrust_api_user=None,
entrust_api_key=None,
entrust_api_cert=None,
entrust_api_cert_key=None,
entrust_api_specification_path=None,
):
"""Create an ECS client""" """Create an ECS client"""
if not YAML_FOUND: if not YAML_FOUND:
raise SessionConfigurationException(missing_required_lib("PyYAML"), exception=YAML_IMP_ERR) raise SessionConfigurationException(
missing_required_lib("PyYAML"), exception=YAML_IMP_ERR
)
if entrust_api_specification_path is None: if entrust_api_specification_path is None:
entrust_api_specification_path = "https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml" entrust_api_specification_path = (
"https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml"
)
# Not functionally necessary with current uses of this module_util, but better to be explicit for future use cases # Not functionally necessary with current uses of this module_util, but better to be explicit for future use cases
entrust_api_user = to_text(entrust_api_user) entrust_api_user = to_text(entrust_api_user)

View File

@@ -3,7 +3,9 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function) from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import abc import abc
@@ -37,19 +39,32 @@ class GPGRunner(object):
def get_fingerprint_from_stdout(stdout): def get_fingerprint_from_stdout(stdout):
lines = stdout.splitlines(False) lines = stdout.splitlines(False)
for line in lines: for line in lines:
if line.startswith('fpr:'): if line.startswith("fpr:"):
parts = line.split(':') parts = line.split(":")
if len(parts) <= 9 or not parts[9]: if len(parts) <= 9 or not parts[9]:
raise GPGError('Result line "{line}" does not have fingerprint as 10th component'.format(line=line)) raise GPGError(
'Result line "{line}" does not have fingerprint as 10th component'.format(
line=line
)
)
return parts[9] return parts[9]
raise GPGError('Cannot extract fingerprint from stdout "{stdout}"'.format(stdout=stdout)) raise GPGError(
'Cannot extract fingerprint from stdout "{stdout}"'.format(stdout=stdout)
)
def get_fingerprint_from_file(gpg_runner, path): def get_fingerprint_from_file(gpg_runner, path):
if not os.path.exists(path): if not os.path.exists(path):
raise GPGError('{path} does not exist'.format(path=path)) raise GPGError("{path} does not exist".format(path=path))
stdout = gpg_runner.run_command( stdout = gpg_runner.run_command(
['--no-keyring', '--with-colons', '--import-options', 'show-only', '--import', path], [
"--no-keyring",
"--with-colons",
"--import-options",
"show-only",
"--import",
path,
],
check_rc=True, check_rc=True,
)[1] )[1]
return get_fingerprint_from_stdout(stdout) return get_fingerprint_from_stdout(stdout)
@@ -57,7 +72,14 @@ def get_fingerprint_from_file(gpg_runner, path):
def get_fingerprint_from_bytes(gpg_runner, content): def get_fingerprint_from_bytes(gpg_runner, content):
stdout = gpg_runner.run_command( stdout = gpg_runner.run_command(
['--no-keyring', '--with-colons', '--import-options', 'show-only', '--import', '/dev/stdin'], [
"--no-keyring",
"--with-colons",
"--import-options",
"show-only",
"--import",
"/dev/stdin",
],
data=content, data=content,
check_rc=True, check_rc=True,
)[1] )[1]

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -14,28 +16,28 @@ import tempfile
def load_file(path, module=None): def load_file(path, module=None):
''' """
Load the file as a bytes string. Load the file as a bytes string.
''' """
try: try:
with open(path, 'rb') as f: with open(path, "rb") as f:
return f.read() return f.read()
except Exception as exc: except Exception as exc:
if module is None: if module is None:
raise raise
module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) module.fail_json("Error while loading {0} - {1}".format(path, str(exc)))
def load_file_if_exists(path, module=None, ignore_errors=False): def load_file_if_exists(path, module=None, ignore_errors=False):
''' """
Load the file as a bytes string. If the file does not exist, ``None`` is returned. Load the file as a bytes string. If the file does not exist, ``None`` is returned.
If ``ignore_errors`` is ``True``, will ignore errors. Otherwise, errors are If ``ignore_errors`` is ``True``, will ignore errors. Otherwise, errors are
raised as exceptions if ``module`` is not specified, and result in ``module.fail_json`` raised as exceptions if ``module`` is not specified, and result in ``module.fail_json``
being called when ``module`` is specified. being called when ``module`` is specified.
''' """
try: try:
with open(path, 'rb') as f: with open(path, "rb") as f:
return f.read() return f.read()
except EnvironmentError as exc: except EnvironmentError as exc:
if exc.errno == errno.ENOENT: if exc.errno == errno.ENOENT:
@@ -44,20 +46,20 @@ def load_file_if_exists(path, module=None, ignore_errors=False):
return None return None
if module is None: if module is None:
raise raise
module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) module.fail_json("Error while loading {0} - {1}".format(path, str(exc)))
except Exception as exc: except Exception as exc:
if ignore_errors: if ignore_errors:
return None return None
if module is None: if module is None:
raise raise
module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) module.fail_json("Error while loading {0} - {1}".format(path, str(exc)))
def write_file(module, content, default_mode=None, path=None): def write_file(module, content, default_mode=None, path=None):
''' """
Writes content into destination file as securely as possible. Writes content into destination file as securely as possible.
Uses file arguments from module. Uses file arguments from module.
''' """
# Find out parameters for file # Find out parameters for file
try: try:
file_args = module.load_file_common_arguments(module.params, path=path) file_args = module.load_file_common_arguments(module.params, path=path)
@@ -66,11 +68,11 @@ def write_file(module, content, default_mode=None, path=None):
# pre-2.10 behavior of module_utils/crypto.py for older Ansible versions. # pre-2.10 behavior of module_utils/crypto.py for older Ansible versions.
file_args = module.load_file_common_arguments(module.params) file_args = module.load_file_common_arguments(module.params)
if path is not None: if path is not None:
file_args['path'] = path file_args["path"] = path
if file_args['mode'] is None: if file_args["mode"] is None:
file_args['mode'] = default_mode file_args["mode"] = default_mode
# Create tempfile name # Create tempfile name
tmp_fd, tmp_name = tempfile.mkstemp(prefix=b'.ansible_tmp') tmp_fd, tmp_name = tempfile.mkstemp(prefix=b".ansible_tmp")
try: try:
os.close(tmp_fd) os.close(tmp_fd)
except Exception: except Exception:
@@ -87,18 +89,22 @@ def write_file(module, content, default_mode=None, path=None):
os.remove(tmp_name) os.remove(tmp_name)
except Exception: except Exception:
pass pass
module.fail_json(msg='Error while writing result into temporary file: {0}'.format(e)) module.fail_json(
msg="Error while writing result into temporary file: {0}".format(e)
)
# Update destination to wanted permissions # Update destination to wanted permissions
if os.path.exists(file_args['path']): if os.path.exists(file_args["path"]):
module.set_fs_attributes_if_different(file_args, False) module.set_fs_attributes_if_different(file_args, False)
# Move tempfile to final destination # Move tempfile to final destination
module.atomic_move(os.path.abspath(tmp_name), os.path.abspath(file_args['path'])) module.atomic_move(
os.path.abspath(tmp_name), os.path.abspath(file_args["path"])
)
# Try to update permissions again # Try to update permissions again
if not module.check_file_absent_if_check_mode(file_args['path']): if not module.check_file_absent_if_check_mode(file_args["path"]):
module.set_fs_attributes_if_different(file_args, False) 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)
except Exception: except Exception:
pass pass
module.fail_json(msg='Error while writing result: {0}'.format(e)) module.fail_json(msg="Error while writing result: {0}".format(e))

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import abc import abc
@@ -13,7 +15,6 @@ import stat
import traceback import traceback
from ansible.module_utils import six from ansible.module_utils import six
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import ( from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
parse_openssh_version, parse_openssh_version,
@@ -43,17 +44,24 @@ def safe_atomic_move(module, path, destination):
def _restore_all_on_failure(f): def _restore_all_on_failure(f):
def backup_and_restore(self, sources_and_destinations, *args, **kwargs): 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)] backups = [
(d, self.module.backup_local(d))
for s, d in sources_and_destinations
if os.path.exists(d)
]
try: try:
f(self, sources_and_destinations, *args, **kwargs) f(self, sources_and_destinations, *args, **kwargs)
except Exception: except Exception:
for destination, backup in backups: for destination, backup in backups:
self.module.atomic_move(os.path.abspath(backup), os.path.abspath(destination)) self.module.atomic_move(
os.path.abspath(backup), os.path.abspath(destination)
)
raise raise
else: else:
for destination, backup in backups: for destination, backup in backups:
self.module.add_cleanup_file(backup) self.module.add_cleanup_file(backup)
return backup_and_restore return backup_and_restore
@@ -84,10 +92,10 @@ class OpensshModule(object):
def result(self): def result(self):
result = self._result result = self._result
result['changed'] = self.changed result["changed"] = self.changed
if self.module._diff: if self.module._diff:
result['diff'] = self.diff result["diff"] = self.diff
return result return result
@@ -106,6 +114,7 @@ class OpensshModule(object):
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
if not self.check_mode: if not self.check_mode:
f(self, *args, **kwargs) f(self, *args, **kwargs)
return wrapper return wrapper
@staticmethod @staticmethod
@@ -113,21 +122,25 @@ class OpensshModule(object):
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
f(self, *args, **kwargs) f(self, *args, **kwargs)
self.changed = True self.changed = True
return wrapper return wrapper
def _check_if_base_dir(self, path): def _check_if_base_dir(self, path):
base_dir = os.path.dirname(path) or '.' base_dir = os.path.dirname(path) or "."
if not os.path.isdir(base_dir): if not os.path.isdir(base_dir):
self.module.fail_json( self.module.fail_json(
name=base_dir, name=base_dir,
msg='The directory %s does not exist or the file is not a directory' % base_dir msg="The directory %s does not exist or the file is not a directory"
% base_dir,
) )
def _get_ssh_version(self): def _get_ssh_version(self):
ssh_bin = self.module.get_bin_path('ssh') ssh_bin = self.module.get_bin_path("ssh")
if not ssh_bin: if not ssh_bin:
return "" return ""
return parse_openssh_version(self.module.run_command([ssh_bin, '-V', '-q'], check_rc=True)[2].strip()) return parse_openssh_version(
self.module.run_command([ssh_bin, "-V", "-q"], check_rc=True)[2].strip()
)
@_restore_all_on_failure @_restore_all_on_failure
def _safe_secure_move(self, sources_and_destinations): def _safe_secure_move(self, sources_and_destinations):
@@ -138,47 +151,63 @@ class OpensshModule(object):
""" """
for source, destination in sources_and_destinations: for source, destination in sources_and_destinations:
if os.path.exists(destination): if os.path.exists(destination):
self.module.atomic_move(os.path.abspath(source), os.path.abspath(destination)) self.module.atomic_move(
os.path.abspath(source), os.path.abspath(destination)
)
else: else:
self.module.preserved_copy(source, destination) self.module.preserved_copy(source, destination)
def _update_permissions(self, path): def _update_permissions(self, path):
file_args = self.module.load_file_common_arguments(self.module.params) file_args = self.module.load_file_common_arguments(self.module.params)
file_args['path'] = path file_args["path"] = path
if not self.module.check_file_absent_if_check_mode(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) self.changed = self.module.set_fs_attributes_if_different(
file_args, self.changed
)
else: else:
self.changed = True self.changed = True
class KeygenCommand(object): class KeygenCommand(object):
def __init__(self, module): def __init__(self, module):
self._bin_path = module.get_bin_path('ssh-keygen', True) self._bin_path = module.get_bin_path("ssh-keygen", True)
self._run_command = module.run_command self._run_command = module.run_command
def generate_certificate(self, certificate_path, identifier, options, pkcs11_provider, principals, def generate_certificate(
serial_number, signature_algorithm, signing_key_path, type, self,
time_parameters, use_agent, **kwargs): certificate_path,
args = [self._bin_path, '-s', signing_key_path, '-P', '', '-I', identifier] 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: if options:
for option in options: for option in options:
args.extend(['-O', option]) args.extend(["-O", option])
if pkcs11_provider: if pkcs11_provider:
args.extend(['-D', pkcs11_provider]) args.extend(["-D", pkcs11_provider])
if principals: if principals:
args.extend(['-n', ','.join(principals)]) args.extend(["-n", ",".join(principals)])
if serial_number is not None: if serial_number is not None:
args.extend(['-z', str(serial_number)]) args.extend(["-z", str(serial_number)])
if type == 'host': if type == "host":
args.extend(['-h']) args.extend(["-h"])
if use_agent: if use_agent:
args.extend(['-U']) args.extend(["-U"])
if time_parameters.validity_string: if time_parameters.validity_string:
args.extend(['-V', time_parameters.validity_string]) args.extend(["-V", time_parameters.validity_string])
if signature_algorithm: if signature_algorithm:
args.extend(['-t', signature_algorithm]) args.extend(["-t", signature_algorithm])
args.append(certificate_path) args.append(certificate_path)
return self._run_command(args, **kwargs) return self._run_command(args, **kwargs)
@@ -186,44 +215,62 @@ class KeygenCommand(object):
def generate_keypair(self, private_key_path, size, type, comment, **kwargs): def generate_keypair(self, private_key_path, size, type, comment, **kwargs):
args = [ args = [
self._bin_path, self._bin_path,
'-q', "-q",
'-N', '', "-N",
'-b', str(size), "",
'-t', type, "-b",
'-f', private_key_path, str(size),
'-C', comment or '' "-t",
type,
"-f",
private_key_path,
"-C",
comment or "",
] ]
# "y" must be entered in response to the "overwrite" prompt # "y" must be entered in response to the "overwrite" prompt
data = 'y' if os.path.exists(private_key_path) else None data = "y" if os.path.exists(private_key_path) else None
return self._run_command(args, data=data, **kwargs) return self._run_command(args, data=data, **kwargs)
def get_certificate_info(self, certificate_path, **kwargs): def get_certificate_info(self, certificate_path, **kwargs):
return self._run_command([self._bin_path, '-L', '-f', 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): def get_matching_public_key(self, private_key_path, **kwargs):
return self._run_command([self._bin_path, '-P', '', '-y', '-f', 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): def get_private_key(self, private_key_path, **kwargs):
return self._run_command([self._bin_path, '-l', '-f', 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, force_new_format=True, **kwargs): def update_comment(
if os.path.exists(private_key_path) and not os.access(private_key_path, os.W_OK): self, private_key_path, comment, force_new_format=True, **kwargs
):
if os.path.exists(private_key_path) and not os.access(
private_key_path, os.W_OK
):
try: try:
os.chmod(private_key_path, stat.S_IWUSR + stat.S_IRUSR) os.chmod(private_key_path, stat.S_IWUSR + stat.S_IRUSR)
except (IOError, OSError) as e: except (IOError, OSError) as e:
raise e("The private key at %s is not writeable preventing a comment update" % private_key_path) raise e(
"The private key at %s is not writeable preventing a comment update"
% private_key_path
)
command = [self._bin_path, '-q'] command = [self._bin_path, "-q"]
if force_new_format: if force_new_format:
command.append('-o') command.append("-o")
command.extend(['-c', '-C', comment, '-f', private_key_path]) command.extend(["-c", "-C", comment, "-f", private_key_path])
return self._run_command(command, **kwargs) return self._run_command(command, **kwargs)
class PrivateKey(object): class PrivateKey(object):
def __init__(self, size, key_type, fingerprint, format=''): def __init__(self, size, key_type, fingerprint, format=""):
self._size = size self._size = size
self._type = key_type self._type = key_type
self._fingerprint = fingerprint self._fingerprint = fingerprint
@@ -257,10 +304,10 @@ class PrivateKey(object):
def to_dict(self): def to_dict(self):
return { return {
'size': self._size, "size": self._size,
'type': self._type, "type": self._type,
'fingerprint': self._fingerprint, "fingerprint": self._fingerprint,
'format': self._format, "format": self._format,
} }
@@ -274,11 +321,17 @@ class PublicKey(object):
if not isinstance(other, type(self)): if not isinstance(other, type(self)):
return NotImplemented return NotImplemented
return all([ return all(
[
self._type_string == other._type_string, self._type_string == other._type_string,
self._data == other._data, self._data == other._data,
(self._comment == other._comment) if self._comment is not None and other._comment is not None else True (
]) (self._comment == other._comment)
if self._comment is not None and other._comment is not None
else True
),
]
)
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self == other
@@ -304,19 +357,19 @@ class PublicKey(object):
@classmethod @classmethod
def from_string(cls, string): def from_string(cls, string):
properties = string.strip('\n').split(' ', 2) properties = string.strip("\n").split(" ", 2)
return cls( return cls(
type_string=properties[0], type_string=properties[0],
data=properties[1], data=properties[1],
comment=properties[2] if len(properties) > 2 else "" comment=properties[2] if len(properties) > 2 else "",
) )
@classmethod @classmethod
def load(cls, path): def load(cls, path):
try: try:
with open(path, 'r') as f: with open(path, "r") as f:
properties = f.read().strip(' \n').split(' ', 2) properties = f.read().strip(" \n").split(" ", 2)
except (IOError, OSError): except (IOError, OSError):
raise raise
@@ -326,25 +379,25 @@ class PublicKey(object):
return cls( return cls(
type_string=properties[0], type_string=properties[0],
data=properties[1], data=properties[1],
comment='' if len(properties) <= 2 else properties[2], comment="" if len(properties) <= 2 else properties[2],
) )
def to_dict(self): def to_dict(self):
return { return {
'comment': self._comment, "comment": self._comment,
'public_key': self._data, "public_key": self._data,
} }
def parse_private_key_format(path): def parse_private_key_format(path):
with open(path, 'r') as file: with open(path, "r") as file:
header = file.readline().strip() header = file.readline().strip()
if header == '-----BEGIN OPENSSH PRIVATE KEY-----': if header == "-----BEGIN OPENSSH PRIVATE KEY-----":
return 'SSH' return "SSH"
elif header == '-----BEGIN PRIVATE KEY-----': elif header == "-----BEGIN PRIVATE KEY-----":
return 'PKCS8' return "PKCS8"
elif header == '-----BEGIN RSA PRIVATE KEY-----': elif header == "-----BEGIN RSA PRIVATE KEY-----":
return 'PKCS1' return "PKCS1"
return '' return ""

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import abc import abc
@@ -13,19 +15,7 @@ import os
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.common.text.converters import to_native, to_text, to_bytes from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.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 ( from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import (
KeygenCommand, KeygenCommand,
OpensshModule, OpensshModule,
@@ -33,11 +23,23 @@ from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.
PublicKey, PublicKey,
parse_private_key_format, parse_private_key_format,
) )
from ansible_collections.community.crypto.plugins.module_utils.openssh.cryptography import (
HAS_OPENSSH_PRIVATE_FORMAT,
HAS_OPENSSH_SUPPORT,
InvalidCommentError,
InvalidPassphraseError,
InvalidPrivateKeyFileError,
OpenSSHError,
OpensshKeypair,
)
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import ( from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
any_in, any_in,
file_mode, file_mode,
secure_write, secure_write,
) )
from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
@@ -46,14 +48,18 @@ class KeypairBackend(OpensshModule):
def __init__(self, module): def __init__(self, module):
super(KeypairBackend, self).__init__(module) super(KeypairBackend, self).__init__(module)
self.comment = self.module.params['comment'] self.comment = self.module.params["comment"]
self.private_key_path = self.module.params['path'] self.private_key_path = self.module.params["path"]
self.public_key_path = self.private_key_path + '.pub' self.public_key_path = self.private_key_path + ".pub"
self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always' self.regenerate = (
self.state = self.module.params['state'] self.module.params["regenerate"]
self.type = self.module.params['type'] 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.size = self._get_size(self.module.params["size"])
self._validate_path() self._validate_path()
self.original_private_key = None self.original_private_key = None
@@ -62,31 +68,35 @@ class KeypairBackend(OpensshModule):
self.public_key = None self.public_key = None
def _get_size(self, size): def _get_size(self, size):
if self.type in ('rsa', 'rsa1'): if self.type in ("rsa", "rsa1"):
result = 4096 if size is None else size result = 4096 if size is None else size
if result < 1024: if result < 1024:
return self.module.fail_json( return self.module.fail_json(
msg="For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. " + 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." + "Attempting to use bit lengths under 1024 will cause the module to fail."
) )
elif self.type == 'dsa': elif self.type == "dsa":
result = 1024 if size is None else size result = 1024 if size is None else size
if result != 1024: if result != 1024:
return self.module.fail_json(msg="DSA keys must be exactly 1024 bits as specified by FIPS 186-2.") return self.module.fail_json(
elif self.type == 'ecdsa': 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 result = 256 if size is None else size
if result not in (256, 384, 521): if result not in (256, 384, 521):
return self.module.fail_json( return self.module.fail_json(
msg="For ECDSA keys, size determines the key length by selecting from one of " + msg="For ECDSA keys, size determines the key length by selecting from one of "
"three elliptic curve sizes: 256, 384 or 521 bits. " + + "three elliptic curve sizes: 256, 384 or 521 bits. "
"Attempting to use bit lengths other than these three values for ECDSA keys will " + + "Attempting to use bit lengths other than these three values for ECDSA keys will "
"cause this module to fail." + "cause this module to fail."
) )
elif self.type == 'ed25519': elif self.type == "ed25519":
# User input is ignored for `key size` when `key type` is ed25519 # User input is ignored for `key size` when `key type` is ed25519
result = 256 result = 256
else: else:
return self.module.fail_json(msg="%s is not a valid value for key type" % self.type) return self.module.fail_json(
msg="%s is not a valid value for key type" % self.type
)
return result return result
@@ -94,13 +104,16 @@ class KeypairBackend(OpensshModule):
self._check_if_base_dir(self.private_key_path) self._check_if_base_dir(self.private_key_path)
if os.path.isdir(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) self.module.fail_json(
msg="%s is a directory. Please specify a path to a file."
% self.private_key_path
)
def _execute(self): def _execute(self):
self.original_private_key = self._load_private_key() self.original_private_key = self._load_private_key()
self.original_public_key = self._load_public_key() self.original_public_key = self._load_public_key()
if self.state == 'present': if self.state == "present":
self._validate_key_load() self._validate_key_load()
if self._should_generate(): if self._should_generate():
@@ -147,13 +160,15 @@ class KeypairBackend(OpensshModule):
return os.path.exists(self.public_key_path) return os.path.exists(self.public_key_path)
def _validate_key_load(self): def _validate_key_load(self):
if (self._private_key_exists() if (
and self.regenerate in ('never', 'fail', 'partial_idempotence') self._private_key_exists()
and (self.original_private_key is None or not self._private_key_readable())): 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( self.module.fail_json(
msg="Unable to read the key. The key is protected with a passphrase or broken. " + 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` " + + "Will not proceed. To force regeneration, call the module with `generate` "
"set to `full_idempotence` or `always`, or with `force=true`." + "set to `full_idempotence` or `always`, or with `force=true`."
) )
@abc.abstractmethod @abc.abstractmethod
@@ -163,17 +178,17 @@ class KeypairBackend(OpensshModule):
def _should_generate(self): def _should_generate(self):
if self.original_private_key is None: if self.original_private_key is None:
return True return True
elif self.regenerate == 'never': elif self.regenerate == "never":
return False return False
elif self.regenerate == 'fail': elif self.regenerate == "fail":
if not self._private_key_valid(): if not self._private_key_valid():
self.module.fail_json( self.module.fail_json(
msg="Key has wrong type and/or size. Will not proceed. " + msg="Key has wrong type and/or size. Will not proceed. "
"To force regeneration, call the module with `generate` set to " + + "To force regeneration, call the module with `generate` set to "
"`partial_idempotence`, `full_idempotence` or `always`, or with `force=true`." + "`partial_idempotence`, `full_idempotence` or `always`, or with `force=true`."
) )
return False return False
elif self.regenerate in ('partial_idempotence', 'full_idempotence'): elif self.regenerate in ("partial_idempotence", "full_idempotence"):
return not self._private_key_valid() return not self._private_key_valid()
else: else:
return True return True
@@ -182,11 +197,13 @@ class KeypairBackend(OpensshModule):
if self.original_private_key is None: if self.original_private_key is None:
return False return False
return all([ return all(
[
self.size == self.original_private_key.size, self.size == self.original_private_key.size,
self.type == self.original_private_key.type, self.type == self.original_private_key.type,
self._private_key_valid_backend(), self._private_key_valid_backend(),
]) ]
)
@abc.abstractmethod @abc.abstractmethod
def _private_key_valid_backend(self): def _private_key_valid_backend(self):
@@ -198,13 +215,20 @@ class KeypairBackend(OpensshModule):
temp_private_key, temp_public_key = self._generate_temp_keypair() temp_private_key, temp_public_key = self._generate_temp_keypair()
try: try:
self._safe_secure_move([(temp_private_key, self.private_key_path), (temp_public_key, self.public_key_path)]) self._safe_secure_move(
[
(temp_private_key, self.private_key_path),
(temp_public_key, self.public_key_path),
]
)
except OSError as e: except OSError as e:
self.module.fail_json(msg=to_native(e)) self.module.fail_json(msg=to_native(e))
def _generate_temp_keypair(self): def _generate_temp_keypair(self):
temp_private_key = os.path.join(self.module.tmpdir, os.path.basename(self.private_key_path)) temp_private_key = os.path.join(
temp_public_key = temp_private_key + '.pub' self.module.tmpdir, os.path.basename(self.private_key_path)
)
temp_public_key = temp_private_key + ".pub"
try: try:
self._generate_keypair(temp_private_key) self._generate_keypair(temp_private_key)
@@ -237,27 +261,33 @@ class KeypairBackend(OpensshModule):
@OpensshModule.skip_if_check_mode @OpensshModule.skip_if_check_mode
def _restore_public_key(self): def _restore_public_key(self):
try: try:
temp_public_key = self._create_temp_public_key(str(self._get_public_key()) + '\n') temp_public_key = self._create_temp_public_key(
self._safe_secure_move([ str(self._get_public_key()) + "\n"
(temp_public_key, self.public_key_path) )
]) self._safe_secure_move([(temp_public_key, self.public_key_path)])
except (IOError, OSError): except (IOError, OSError):
self.module.fail_json( self.module.fail_json(
msg="The public key is missing or does not match the private key. " + msg="The public key is missing or does not match the private key. "
"Unable to regenerate the public key." + "Unable to regenerate the public key."
) )
if self.comment: if self.comment:
self._update_comment() self._update_comment()
def _create_temp_public_key(self, content): def _create_temp_public_key(self, content):
temp_public_key = os.path.join(self.module.tmpdir, os.path.basename(self.public_key_path)) temp_public_key = os.path.join(
self.module.tmpdir, os.path.basename(self.public_key_path)
)
default_permissions = 0o644 default_permissions = 0o644
existing_permissions = file_mode(self.public_key_path) existing_permissions = file_mode(self.public_key_path)
try: try:
secure_write(temp_public_key, existing_permissions or default_permissions, to_bytes(content)) secure_write(
temp_public_key,
existing_permissions or default_permissions,
to_bytes(content),
)
except (IOError, OSError) as e: except (IOError, OSError) as e:
self.module.fail_json(msg=to_native(e)) self.module.fail_json(msg=to_native(e))
self.module.add_cleanup_file(temp_public_key) self.module.add_cleanup_file(temp_public_key)
@@ -288,25 +318,29 @@ class KeypairBackend(OpensshModule):
public_key = self.public_key or self.original_public_key public_key = self.public_key or self.original_public_key
return { return {
'size': self.size, "size": self.size,
'type': self.type, "type": self.type,
'filename': self.private_key_path, "filename": self.private_key_path,
'fingerprint': private_key.fingerprint if private_key else '', "fingerprint": private_key.fingerprint if private_key else "",
'public_key': str(public_key) if public_key else '', "public_key": str(public_key) if public_key else "",
'comment': public_key.comment if public_key else '', "comment": public_key.comment if public_key else "",
} }
@property @property
def diff(self): def diff(self):
before = self.original_private_key.to_dict() if self.original_private_key else {} before = (
before.update(self.original_public_key.to_dict() if self.original_public_key else {}) 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 = self.private_key.to_dict() if self.private_key else {}
after.update(self.public_key.to_dict() if self.public_key else {}) after.update(self.public_key.to_dict() if self.public_key else {})
return { return {
'before': before, "before": before,
'after': after, "after": after,
} }
@@ -314,36 +348,59 @@ class KeypairBackendOpensshBin(KeypairBackend):
def __init__(self, module): def __init__(self, module):
super(KeypairBackendOpensshBin, self).__init__(module) super(KeypairBackendOpensshBin, self).__init__(module)
if self.module.params['private_key_format'] != 'auto': if self.module.params["private_key_format"] != "auto":
self.module.fail_json( self.module.fail_json(
msg="'auto' is the only valid option for " + msg="'auto' is the only valid option for "
"'private_key_format' when 'backend' is not 'cryptography'" + "'private_key_format' when 'backend' is not 'cryptography'"
) )
self.ssh_keygen = KeygenCommand(self.module) self.ssh_keygen = KeygenCommand(self.module)
def _generate_keypair(self, private_key_path): def _generate_keypair(self, private_key_path):
self.ssh_keygen.generate_keypair(private_key_path, self.size, self.type, self.comment, check_rc=True) self.ssh_keygen.generate_keypair(
private_key_path, self.size, self.type, self.comment, check_rc=True
)
def _get_private_key(self): def _get_private_key(self):
rc, private_key_content, err = self.ssh_keygen.get_private_key(self.private_key_path, check_rc=False) rc, private_key_content, err = self.ssh_keygen.get_private_key(
self.private_key_path, check_rc=False
)
if rc != 0: if rc != 0:
raise ValueError(err) raise ValueError(err)
return PrivateKey.from_string(private_key_content) return PrivateKey.from_string(private_key_content)
def _get_public_key(self): def _get_public_key(self):
public_key_content = self.ssh_keygen.get_matching_public_key(self.private_key_path, check_rc=True)[1] public_key_content = self.ssh_keygen.get_matching_public_key(
self.private_key_path, check_rc=True
)[1]
return PublicKey.from_string(public_key_content) return PublicKey.from_string(public_key_content)
def _private_key_readable(self): def _private_key_readable(self):
rc, stdout, stderr = self.ssh_keygen.get_matching_public_key(self.private_key_path, check_rc=False) rc, stdout, stderr = self.ssh_keygen.get_matching_public_key(
return not (rc == 255 or any_in(stderr, 'is not a public key file', 'incorrect passphrase', 'load failed')) self.private_key_path, check_rc=False
)
return not (
rc == 255
or any_in(
stderr,
"is not a public key file",
"incorrect passphrase",
"load failed",
)
)
def _update_comment(self): def _update_comment(self):
try: try:
ssh_version = self._get_ssh_version() or "7.8" ssh_version = self._get_ssh_version() or "7.8"
force_new_format = LooseVersion('6.5') <= LooseVersion(ssh_version) < LooseVersion('7.8') force_new_format = (
self.ssh_keygen.update_comment(self.private_key_path, self.comment, force_new_format=force_new_format, check_rc=True) LooseVersion("6.5") <= LooseVersion(ssh_version) < LooseVersion("7.8")
)
self.ssh_keygen.update_comment(
self.private_key_path,
self.comment,
force_new_format=force_new_format,
check_rc=True,
)
except (IOError, OSError) as e: except (IOError, OSError) as e:
self.module.fail_json(msg=to_native(e)) self.module.fail_json(msg=to_native(e))
@@ -355,30 +412,41 @@ class KeypairBackendCryptography(KeypairBackend):
def __init__(self, module): def __init__(self, module):
super(KeypairBackendCryptography, self).__init__(module) super(KeypairBackendCryptography, self).__init__(module)
if self.type == 'rsa1': if self.type == "rsa1":
self.module.fail_json(msg="RSA1 keys are not supported by the cryptography backend") 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.passphrase = (
self.private_key_format = self._get_key_format(module.params['private_key_format']) 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): def _get_key_format(self, key_format):
result = 'SSH' result = "SSH"
if key_format == 'auto': if key_format == "auto":
# Default to OpenSSH 7.8 compatibility when OpenSSH is not installed # Default to OpenSSH 7.8 compatibility when OpenSSH is not installed
ssh_version = self._get_ssh_version() or "7.8" ssh_version = self._get_ssh_version() or "7.8"
if LooseVersion(ssh_version) < LooseVersion("7.8") and self.type != 'ed25519': if (
LooseVersion(ssh_version) < LooseVersion("7.8")
and self.type != "ed25519"
):
# OpenSSH made SSH formatted private keys available in version 6.5, # OpenSSH made SSH formatted private keys available in version 6.5,
# but still defaulted to PKCS1 format with the exception of ed25519 keys # but still defaulted to PKCS1 format with the exception of ed25519 keys
result = 'PKCS1' result = "PKCS1"
if result == 'SSH' and not HAS_OPENSSH_PRIVATE_FORMAT: if result == "SSH" and not HAS_OPENSSH_PRIVATE_FORMAT:
self.module.fail_json( self.module.fail_json(
msg=missing_required_lib( msg=missing_required_lib(
'cryptography >= 3.0', "cryptography >= 3.0",
reason="to load/dump private keys in the default OpenSSH format for OpenSSH >= 7.8 " + reason="to load/dump private keys in the default OpenSSH format for OpenSSH >= 7.8 "
"or for ed25519 keys" + "or for ed25519 keys",
) )
) )
else: else:
@@ -391,7 +459,7 @@ class KeypairBackendCryptography(KeypairBackend):
keytype=self.type, keytype=self.type,
size=self.size, size=self.size,
passphrase=self.passphrase, passphrase=self.passphrase,
comment=self.comment or '', comment=self.comment or "",
) )
encoded_private_key = OpensshKeypair.encode_openssh_privatekey( encoded_private_key = OpensshKeypair.encode_openssh_privatekey(
@@ -399,22 +467,28 @@ class KeypairBackendCryptography(KeypairBackend):
) )
secure_write(private_key_path, 0o600, encoded_private_key) secure_write(private_key_path, 0o600, encoded_private_key)
public_key_path = private_key_path + '.pub' public_key_path = private_key_path + ".pub"
secure_write(public_key_path, 0o644, keypair.public_key) secure_write(public_key_path, 0o644, keypair.public_key)
def _get_private_key(self): def _get_private_key(self):
keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True) keypair = OpensshKeypair.load(
path=self.private_key_path, passphrase=self.passphrase, no_public_key=True
)
return PrivateKey( return PrivateKey(
size=keypair.size, size=keypair.size,
key_type=keypair.key_type, key_type=keypair.key_type,
fingerprint=keypair.fingerprint, fingerprint=keypair.fingerprint,
format=parse_private_key_format(self.private_key_path) format=parse_private_key_format(self.private_key_path),
) )
def _get_public_key(self): def _get_public_key(self):
try: try:
keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True) keypair = OpensshKeypair.load(
path=self.private_key_path,
passphrase=self.passphrase,
no_public_key=True,
)
except OpenSSHError: except OpenSSHError:
# Simulates the null output of ssh-keygen # Simulates the null output of ssh-keygen
return "" return ""
@@ -423,7 +497,11 @@ class KeypairBackendCryptography(KeypairBackend):
def _private_key_readable(self): def _private_key_readable(self):
try: try:
OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True) OpensshKeypair.load(
path=self.private_key_path,
passphrase=self.passphrase,
no_public_key=True,
)
except (InvalidPrivateKeyFileError, InvalidPassphraseError): except (InvalidPrivateKeyFileError, InvalidPassphraseError):
return False return False
@@ -431,7 +509,9 @@ class KeypairBackendCryptography(KeypairBackend):
# when loading an unencrypted key # when loading an unencrypted key
if self.passphrase: if self.passphrase:
try: try:
OpensshKeypair.load(path=self.private_key_path, passphrase=None, no_public_key=True) OpensshKeypair.load(
path=self.private_key_path, passphrase=None, no_public_key=True
)
except (InvalidPrivateKeyFileError, InvalidPassphraseError): except (InvalidPrivateKeyFileError, InvalidPassphraseError):
return True return True
else: else:
@@ -440,14 +520,16 @@ class KeypairBackendCryptography(KeypairBackend):
return True return True
def _update_comment(self): def _update_comment(self):
keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True) keypair = OpensshKeypair.load(
path=self.private_key_path, passphrase=self.passphrase, no_public_key=True
)
try: try:
keypair.comment = self.comment keypair.comment = self.comment
except InvalidCommentError as e: except InvalidCommentError as e:
self.module.fail_json(msg=to_native(e)) self.module.fail_json(msg=to_native(e))
try: try:
temp_public_key = self._create_temp_public_key(keypair.public_key + b'\n') temp_public_key = self._create_temp_public_key(keypair.public_key + b"\n")
self._safe_secure_move([(temp_public_key, self.public_key_path)]) self._safe_secure_move([(temp_public_key, self.public_key_path)])
except (IOError, OSError) as e: except (IOError, OSError) as e:
self.module.fail_json(msg=to_native(e)) self.module.fail_json(msg=to_native(e))
@@ -455,7 +537,7 @@ class KeypairBackendCryptography(KeypairBackend):
def _private_key_valid_backend(self): def _private_key_valid_backend(self):
# avoids breaking behavior and prevents # avoids breaking behavior and prevents
# automatic conversions with OpenSSH upgrades # automatic conversions with OpenSSH upgrades
if self.module.params['private_key_format'] == 'auto': if self.module.params["private_key_format"] == "auto":
return True return True
return self.private_key_format == self.original_private_key.format return self.private_key_format == self.original_private_key.format
@@ -463,24 +545,26 @@ class KeypairBackendCryptography(KeypairBackend):
def select_backend(module, backend): def select_backend(module, backend):
can_use_cryptography = HAS_OPENSSH_SUPPORT can_use_cryptography = HAS_OPENSSH_SUPPORT
can_use_opensshbin = bool(module.get_bin_path('ssh-keygen')) can_use_opensshbin = bool(module.get_bin_path("ssh-keygen"))
if backend == 'auto': if backend == "auto":
if can_use_opensshbin and not module.params['passphrase']: if can_use_opensshbin and not module.params["passphrase"]:
backend = 'opensshbin' backend = "opensshbin"
elif can_use_cryptography: elif can_use_cryptography:
backend = 'cryptography' backend = "cryptography"
else: else:
module.fail_json(msg="Cannot find either the OpenSSH binary in the PATH " + module.fail_json(
"or cryptography >= 2.6 installed on this system") msg="Cannot find either the OpenSSH binary in the PATH "
+ "or cryptography >= 2.6 installed on this system"
)
if backend == 'opensshbin': if backend == "opensshbin":
if not can_use_opensshbin: if not can_use_opensshbin:
module.fail_json(msg="Cannot find the OpenSSH binary in the PATH") module.fail_json(msg="Cannot find the OpenSSH binary in the PATH")
return backend, KeypairBackendOpensshBin(module) return backend, KeypairBackendOpensshBin(module)
elif backend == 'cryptography': elif backend == "cryptography":
if not can_use_cryptography: if not can_use_cryptography:
module.fail_json(msg=missing_required_lib("cryptography >= 2.6")) module.fail_json(msg=missing_required_lib("cryptography >= 2.6"))
return backend, KeypairBackendCryptography(module) return backend, KeypairBackendCryptography(module)
else: else:
raise ValueError('Unsupported value for backend: {0}'.format(backend)) raise ValueError("Unsupported value for backend: {0}".format(backend))

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
# Protocol References # Protocol References
@@ -35,65 +37,70 @@ from ansible_collections.community.crypto.plugins.module_utils.openssh.utils imp
OpensshParser, OpensshParser,
_OpensshWriter, _OpensshWriter,
) )
from ansible_collections.community.crypto.plugins.module_utils.time import UTC as _UTC
from ansible_collections.community.crypto.plugins.module_utils.time import ( from ansible_collections.community.crypto.plugins.module_utils.time import (
add_or_remove_timezone as _add_or_remove_timezone, add_or_remove_timezone as _add_or_remove_timezone,
convert_relative_to_datetime,
UTC as _UTC,
) )
from ansible_collections.community.crypto.plugins.module_utils.time import (
convert_relative_to_datetime,
)
# See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD # See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
_USER_TYPE = 1 _USER_TYPE = 1
_HOST_TYPE = 2 _HOST_TYPE = 2
_SSH_TYPE_STRINGS = { _SSH_TYPE_STRINGS = {
'rsa': b"ssh-rsa", "rsa": b"ssh-rsa",
'dsa': b"ssh-dss", "dsa": b"ssh-dss",
'ecdsa-nistp256': b"ecdsa-sha2-nistp256", "ecdsa-nistp256": b"ecdsa-sha2-nistp256",
'ecdsa-nistp384': b"ecdsa-sha2-nistp384", "ecdsa-nistp384": b"ecdsa-sha2-nistp384",
'ecdsa-nistp521': b"ecdsa-sha2-nistp521", "ecdsa-nistp521": b"ecdsa-sha2-nistp521",
'ed25519': b"ssh-ed25519", "ed25519": b"ssh-ed25519",
} }
_CERT_SUFFIX_V01 = b"-cert-v01@openssh.com" _CERT_SUFFIX_V01 = b"-cert-v01@openssh.com"
# See https://datatracker.ietf.org/doc/html/rfc5656#section-6.1 # See https://datatracker.ietf.org/doc/html/rfc5656#section-6.1
_ECDSA_CURVE_IDENTIFIERS = { _ECDSA_CURVE_IDENTIFIERS = {
'ecdsa-nistp256': b'nistp256', "ecdsa-nistp256": b"nistp256",
'ecdsa-nistp384': b'nistp384', "ecdsa-nistp384": b"nistp384",
'ecdsa-nistp521': b'nistp521', "ecdsa-nistp521": b"nistp521",
} }
_ECDSA_CURVE_IDENTIFIERS_LOOKUP = { _ECDSA_CURVE_IDENTIFIERS_LOOKUP = {
b'nistp256': 'ecdsa-nistp256', b"nistp256": "ecdsa-nistp256",
b'nistp384': 'ecdsa-nistp384', b"nistp384": "ecdsa-nistp384",
b'nistp521': 'ecdsa-nistp521', b"nistp521": "ecdsa-nistp521",
} }
_USE_TIMEZONE = sys.version_info >= (3, 6) _USE_TIMEZONE = sys.version_info >= (3, 6)
_ALWAYS = _add_or_remove_timezone(datetime(1970, 1, 1), with_timezone=_USE_TIMEZONE) _ALWAYS = _add_or_remove_timezone(datetime(1970, 1, 1), with_timezone=_USE_TIMEZONE)
_FOREVER = datetime(9999, 12, 31, 23, 59, 59, 999999, _UTC) if _USE_TIMEZONE else datetime.max _FOREVER = (
datetime(9999, 12, 31, 23, 59, 59, 999999, _UTC) if _USE_TIMEZONE else datetime.max
)
_CRITICAL_OPTIONS = ( _CRITICAL_OPTIONS = (
'force-command', "force-command",
'source-address', "source-address",
'verify-required', "verify-required",
) )
_DIRECTIVES = ( _DIRECTIVES = (
'clear', "clear",
'no-x11-forwarding', "no-x11-forwarding",
'no-agent-forwarding', "no-agent-forwarding",
'no-port-forwarding', "no-port-forwarding",
'no-pty', "no-pty",
'no-user-rc', "no-user-rc",
) )
_EXTENSIONS = ( _EXTENSIONS = (
'permit-x11-forwarding', "permit-x11-forwarding",
'permit-agent-forwarding', "permit-agent-forwarding",
'permit-port-forwarding', "permit-port-forwarding",
'permit-pty', "permit-pty",
'permit-user-rc' "permit-user-rc",
) )
if six.PY3: if six.PY3:
@@ -106,13 +113,19 @@ class OpensshCertificateTimeParameters(object):
self._valid_to = self.to_datetime(valid_to) self._valid_to = self.to_datetime(valid_to)
if self._valid_from > self._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)) raise ValueError(
"Valid from: %s must not be greater than Valid to: %s"
% (valid_from, valid_to)
)
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, type(self)): if not isinstance(other, type(self)):
return NotImplemented return NotImplemented
else: else:
return self._valid_from == other._valid_from and self._valid_to == other._valid_to return (
self._valid_from == other._valid_from
and self._valid_to == other._valid_to
)
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self == other
@@ -121,7 +134,8 @@ class OpensshCertificateTimeParameters(object):
def validity_string(self): def validity_string(self):
if not (self._valid_from == _ALWAYS and self._valid_to == _FOREVER): if not (self._valid_from == _ALWAYS and self._valid_to == _FOREVER):
return "%s:%s" % ( return "%s:%s" % (
self.valid_from(date_format='openssh'), self.valid_to(date_format='openssh') self.valid_from(date_format="openssh"),
self.valid_to(date_format="openssh"),
) )
return "" return ""
@@ -139,16 +153,22 @@ class OpensshCertificateTimeParameters(object):
@staticmethod @staticmethod
def format_datetime(dt, date_format): def format_datetime(dt, date_format):
if date_format in ('human_readable', 'openssh'): if date_format in ("human_readable", "openssh"):
if dt == _ALWAYS: if dt == _ALWAYS:
result = 'always' result = "always"
elif dt == _FOREVER: elif dt == _FOREVER:
result = 'forever' result = "forever"
else: else:
result = dt.isoformat().replace('+00:00', '') if date_format == 'human_readable' else dt.strftime("%Y%m%d%H%M%S") result = (
elif date_format == 'timestamp': dt.isoformat().replace("+00:00", "")
if date_format == "human_readable"
else dt.strftime("%Y%m%d%H%M%S")
)
elif date_format == "timestamp":
td = dt - _ALWAYS td = dt - _ALWAYS
result = int((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6) result = int(
(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
)
else: else:
raise ValueError("%s is not a valid format" % date_format) raise ValueError("%s is not a valid format" % date_format)
return result return result
@@ -157,12 +177,17 @@ class OpensshCertificateTimeParameters(object):
def to_datetime(time_string_or_timestamp): def to_datetime(time_string_or_timestamp):
try: try:
if isinstance(time_string_or_timestamp, six.string_types): if isinstance(time_string_or_timestamp, six.string_types):
result = OpensshCertificateTimeParameters._time_string_to_datetime(time_string_or_timestamp.strip()) result = OpensshCertificateTimeParameters._time_string_to_datetime(
time_string_or_timestamp.strip()
)
elif isinstance(time_string_or_timestamp, (long, int)): elif isinstance(time_string_or_timestamp, (long, int)):
result = OpensshCertificateTimeParameters._timestamp_to_datetime(time_string_or_timestamp) result = OpensshCertificateTimeParameters._timestamp_to_datetime(
time_string_or_timestamp
)
else: else:
raise ValueError( raise ValueError(
"Value must be of type (str, unicode, int, long) not %s" % type(time_string_or_timestamp) "Value must be of type (str, unicode, int, long) not %s"
% type(time_string_or_timestamp)
) )
except ValueError: except ValueError:
raise raise
@@ -177,26 +202,33 @@ class OpensshCertificateTimeParameters(object):
else: else:
try: try:
if _USE_TIMEZONE: if _USE_TIMEZONE:
result = datetime.fromtimestamp(timestamp, tz=_datetime.timezone.utc) result = datetime.fromtimestamp(
timestamp, tz=_datetime.timezone.utc
)
else: else:
result = datetime.utcfromtimestamp(timestamp) result = datetime.utcfromtimestamp(timestamp)
except OverflowError as e: except OverflowError:
raise ValueError raise ValueError
return result return result
@staticmethod @staticmethod
def _time_string_to_datetime(time_string): def _time_string_to_datetime(time_string):
result = None result = None
if time_string == 'always': if time_string == "always":
result = _ALWAYS result = _ALWAYS
elif time_string == 'forever': elif time_string == "forever":
result = _FOREVER result = _FOREVER
elif is_relative_time_string(time_string): elif is_relative_time_string(time_string):
result = convert_relative_to_datetime(time_string, with_timezone=_USE_TIMEZONE) result = convert_relative_to_datetime(
time_string, with_timezone=_USE_TIMEZONE
)
else: else:
for time_format in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"): for time_format in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
try: try:
result = _add_or_remove_timezone(datetime.strptime(time_string, time_format), with_timezone=_USE_TIMEZONE) result = _add_or_remove_timezone(
datetime.strptime(time_string, time_format),
with_timezone=_USE_TIMEZONE,
)
except ValueError: except ValueError:
pass pass
if result is None: if result is None:
@@ -206,7 +238,7 @@ class OpensshCertificateTimeParameters(object):
class OpensshCertificateOption(object): class OpensshCertificateOption(object):
def __init__(self, option_type, name, data): def __init__(self, option_type, name, data):
if option_type not in ('critical', 'extension'): if option_type not in ("critical", "extension"):
raise ValueError("type must be either 'critical' or 'extension'") raise ValueError("type must be either 'critical' or 'extension'")
if not isinstance(name, six.string_types): if not isinstance(name, six.string_types):
@@ -223,11 +255,13 @@ class OpensshCertificateOption(object):
if not isinstance(other, type(self)): if not isinstance(other, type(self)):
return NotImplemented return NotImplemented
return all([ return all(
[
self._option_type == other._option_type, self._option_type == other._option_type,
self._name == other._name, self._name == other._name,
self._data == other._data, self._data == other._data,
]) ]
)
def __hash__(self): def __hash__(self):
return hash((self._option_type, self._name, self._data)) return hash((self._option_type, self._name, self._data))
@@ -255,31 +289,35 @@ class OpensshCertificateOption(object):
@classmethod @classmethod
def from_string(cls, option_string): def from_string(cls, option_string):
if not isinstance(option_string, six.string_types): if not isinstance(option_string, six.string_types):
raise ValueError("option_string must be a string not %s" % type(option_string)) raise ValueError(
"option_string must be a string not %s" % type(option_string)
)
option_type = None option_type = None
if ':' in option_string: if ":" in option_string:
option_type, value = option_string.strip().split(':', 1) option_type, value = option_string.strip().split(":", 1)
if '=' in value: if "=" in value:
name, data = value.split('=', 1) name, data = value.split("=", 1)
else: else:
name, data = value, '' name, data = value, ""
elif '=' in option_string: elif "=" in option_string:
name, data = option_string.strip().split('=', 1) name, data = option_string.strip().split("=", 1)
else: else:
name, data = option_string.strip(), '' name, data = option_string.strip(), ""
return cls( return cls(
option_type=option_type or get_option_type(name.lower()), option_type=option_type or get_option_type(name.lower()),
name=name, name=name,
data=data data=data,
) )
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class OpensshCertificateInfo: class OpensshCertificateInfo:
"""Encapsulates all certificate information which is signed by a CA key""" """Encapsulates all certificate information which is signed by a CA key"""
def __init__(self,
def __init__(
self,
nonce=None, nonce=None,
serial=None, serial=None,
cert_type=None, cert_type=None,
@@ -290,7 +328,8 @@ class OpensshCertificateInfo:
critical_options=None, critical_options=None,
extensions=None, extensions=None,
reserved=None, reserved=None,
signing_key=None): signing_key=None,
):
self.nonce = nonce self.nonce = nonce
self.serial = serial self.serial = serial
self._cert_type = cert_type self._cert_type = cert_type
@@ -308,17 +347,17 @@ class OpensshCertificateInfo:
@property @property
def cert_type(self): def cert_type(self):
if self._cert_type == _USER_TYPE: if self._cert_type == _USER_TYPE:
return 'user' return "user"
elif self._cert_type == _HOST_TYPE: elif self._cert_type == _HOST_TYPE:
return 'host' return "host"
else: else:
return '' return ""
@cert_type.setter @cert_type.setter
def cert_type(self, cert_type): def cert_type(self, cert_type):
if cert_type == 'user' or cert_type == _USER_TYPE: if cert_type == "user" or cert_type == _USER_TYPE:
self._cert_type = _USER_TYPE self._cert_type = _USER_TYPE
elif cert_type == 'host' or cert_type == _HOST_TYPE: elif cert_type == "host" or cert_type == _HOST_TYPE:
self._cert_type = _HOST_TYPE self._cert_type = _HOST_TYPE
else: else:
raise ValueError("%s is not a valid certificate type" % cert_type) raise ValueError("%s is not a valid certificate type" % cert_type)
@@ -338,17 +377,17 @@ class OpensshCertificateInfo:
class OpensshRSACertificateInfo(OpensshCertificateInfo): class OpensshRSACertificateInfo(OpensshCertificateInfo):
def __init__(self, e=None, n=None, **kwargs): def __init__(self, e=None, n=None, **kwargs):
super(OpensshRSACertificateInfo, self).__init__(**kwargs) super(OpensshRSACertificateInfo, self).__init__(**kwargs)
self.type_string = _SSH_TYPE_STRINGS['rsa'] + _CERT_SUFFIX_V01 self.type_string = _SSH_TYPE_STRINGS["rsa"] + _CERT_SUFFIX_V01
self.e = e self.e = e
self.n = n self.n = n
# See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 # See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
def public_key_fingerprint(self): def public_key_fingerprint(self):
if any([self.e is None, self.n is None]): if any([self.e is None, self.n is None]):
return b'' return b""
writer = _OpensshWriter() writer = _OpensshWriter()
writer.string(_SSH_TYPE_STRINGS['rsa']) writer.string(_SSH_TYPE_STRINGS["rsa"])
writer.mpint(self.e) writer.mpint(self.e)
writer.mpint(self.n) writer.mpint(self.n)
@@ -362,7 +401,7 @@ class OpensshRSACertificateInfo(OpensshCertificateInfo):
class OpensshDSACertificateInfo(OpensshCertificateInfo): class OpensshDSACertificateInfo(OpensshCertificateInfo):
def __init__(self, p=None, q=None, g=None, y=None, **kwargs): def __init__(self, p=None, q=None, g=None, y=None, **kwargs):
super(OpensshDSACertificateInfo, self).__init__(**kwargs) super(OpensshDSACertificateInfo, self).__init__(**kwargs)
self.type_string = _SSH_TYPE_STRINGS['dsa'] + _CERT_SUFFIX_V01 self.type_string = _SSH_TYPE_STRINGS["dsa"] + _CERT_SUFFIX_V01
self.p = p self.p = p
self.q = q self.q = q
self.g = g self.g = g
@@ -371,10 +410,10 @@ class OpensshDSACertificateInfo(OpensshCertificateInfo):
# See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 # See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
def public_key_fingerprint(self): def public_key_fingerprint(self):
if any([self.p is None, self.q is None, self.g is None, self.y is None]): if any([self.p is None, self.q is None, self.g is None, self.y is None]):
return b'' return b""
writer = _OpensshWriter() writer = _OpensshWriter()
writer.string(_SSH_TYPE_STRINGS['dsa']) writer.string(_SSH_TYPE_STRINGS["dsa"])
writer.mpint(self.p) writer.mpint(self.p)
writer.mpint(self.q) writer.mpint(self.q)
writer.mpint(self.g) writer.mpint(self.g)
@@ -406,16 +445,20 @@ class OpensshECDSACertificateInfo(OpensshCertificateInfo):
def curve(self, curve): def curve(self, curve):
if curve in _ECDSA_CURVE_IDENTIFIERS.values(): if curve in _ECDSA_CURVE_IDENTIFIERS.values():
self._curve = curve self._curve = curve
self.type_string = _SSH_TYPE_STRINGS[_ECDSA_CURVE_IDENTIFIERS_LOOKUP[curve]] + _CERT_SUFFIX_V01 self.type_string = (
_SSH_TYPE_STRINGS[_ECDSA_CURVE_IDENTIFIERS_LOOKUP[curve]]
+ _CERT_SUFFIX_V01
)
else: else:
raise ValueError( raise ValueError(
"Curve must be one of %s" % (b','.join(list(_ECDSA_CURVE_IDENTIFIERS.values()))).decode('UTF-8') "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 # See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
def public_key_fingerprint(self): def public_key_fingerprint(self):
if any([self.curve is None, self.public_key is None]): if any([self.curve is None, self.public_key is None]):
return b'' return b""
writer = _OpensshWriter() writer = _OpensshWriter()
writer.string(_SSH_TYPE_STRINGS[_ECDSA_CURVE_IDENTIFIERS_LOOKUP[self.curve]]) writer.string(_SSH_TYPE_STRINGS[_ECDSA_CURVE_IDENTIFIERS_LOOKUP[self.curve]])
@@ -432,15 +475,15 @@ class OpensshECDSACertificateInfo(OpensshCertificateInfo):
class OpensshED25519CertificateInfo(OpensshCertificateInfo): class OpensshED25519CertificateInfo(OpensshCertificateInfo):
def __init__(self, pk=None, **kwargs): def __init__(self, pk=None, **kwargs):
super(OpensshED25519CertificateInfo, self).__init__(**kwargs) super(OpensshED25519CertificateInfo, self).__init__(**kwargs)
self.type_string = _SSH_TYPE_STRINGS['ed25519'] + _CERT_SUFFIX_V01 self.type_string = _SSH_TYPE_STRINGS["ed25519"] + _CERT_SUFFIX_V01
self.pk = pk self.pk = pk
def public_key_fingerprint(self): def public_key_fingerprint(self):
if self.pk is None: if self.pk is None:
return b'' return b""
writer = _OpensshWriter() writer = _OpensshWriter()
writer.string(_SSH_TYPE_STRINGS['ed25519']) writer.string(_SSH_TYPE_STRINGS["ed25519"])
writer.string(self.pk) writer.string(self.pk)
return fingerprint(writer.bytes()) return fingerprint(writer.bytes())
@@ -452,6 +495,7 @@ class OpensshED25519CertificateInfo(OpensshCertificateInfo):
# See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD # See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
class OpensshCertificate(object): class OpensshCertificate(object):
"""Encapsulates a formatted OpenSSH certificate including signature and signing key""" """Encapsulates a formatted OpenSSH certificate including signature and signing key"""
def __init__(self, cert_info, signature): def __init__(self, cert_info, signature):
self._cert_info = cert_info self._cert_info = cert_info
@@ -463,13 +507,13 @@ class OpensshCertificate(object):
raise ValueError("%s is not a valid path." % path) raise ValueError("%s is not a valid path." % path)
try: try:
with open(path, 'rb') as cert_file: with open(path, "rb") as cert_file:
data = cert_file.read() data = cert_file.read()
except (IOError, OSError) as e: except (IOError, OSError) as e:
raise ValueError("%s cannot be opened for reading: %s" % (path, e)) raise ValueError("%s cannot be opened for reading: %s" % (path, e))
try: try:
format_identifier, b64_cert = data.split(b' ')[:2] format_identifier, b64_cert = data.split(b" ")[:2]
cert = binascii.a2b_base64(b64_cert) cert = binascii.a2b_base64(b64_cert)
except (binascii.Error, ValueError): except (binascii.Error, ValueError):
raise ValueError("Certificate not in OpenSSH format") raise ValueError("Certificate not in OpenSSH format")
@@ -479,7 +523,9 @@ class OpensshCertificate(object):
pub_key_type = key_type pub_key_type = key_type
break break
else: else:
raise ValueError("Invalid certificate format identifier: %s" % format_identifier) raise ValueError(
"Invalid certificate format identifier: %s" % format_identifier
)
parser = OpensshParser(cert) parser = OpensshParser(cert)
@@ -494,7 +540,8 @@ class OpensshCertificate(object):
if parser.remaining_bytes(): if parser.remaining_bytes():
raise ValueError( raise ValueError(
"%s bytes of additional data was not parsed while loading %s" % (parser.remaining_bytes(), path) "%s bytes of additional data was not parsed while loading %s"
% (parser.remaining_bytes(), path)
) )
return cls( return cls(
@@ -541,12 +588,16 @@ class OpensshCertificate(object):
@property @property
def critical_options(self): def critical_options(self):
return [ return [
OpensshCertificateOption('critical', to_text(n), to_text(d)) for n, d in self._cert_info.critical_options OpensshCertificateOption("critical", to_text(n), to_text(d))
for n, d in self._cert_info.critical_options
] ]
@property @property
def extensions(self): def extensions(self):
return [OpensshCertificateOption('extension', to_text(n), to_text(d)) for n, d in self._cert_info.extensions] return [
OpensshCertificateOption("extension", to_text(n), to_text(d))
for n, d in self._cert_info.extensions
]
@property @property
def reserved(self): def reserved(self):
@@ -559,7 +610,7 @@ class OpensshCertificate(object):
@property @property
def signature_type(self): def signature_type(self):
signature_data = OpensshParser.signature_data(self.signature) signature_data = OpensshParser.signature_data(self.signature)
return to_text(signature_data['signature_type']) return to_text(signature_data["signature_type"])
@staticmethod @staticmethod
def _parse_cert_info(pub_key_type, parser): def _parse_cert_info(pub_key_type, parser):
@@ -581,23 +632,24 @@ class OpensshCertificate(object):
def to_dict(self): def to_dict(self):
time_parameters = OpensshCertificateTimeParameters( time_parameters = OpensshCertificateTimeParameters(
valid_from=self.valid_after, valid_from=self.valid_after, valid_to=self.valid_before
valid_to=self.valid_before
) )
return { return {
'type_string': self.type_string, "type_string": self.type_string,
'nonce': self.nonce, "nonce": self.nonce,
'serial': self.serial, "serial": self.serial,
'cert_type': self.type, "cert_type": self.type,
'identifier': self.key_id, "identifier": self.key_id,
'principals': self.principals, "principals": self.principals,
'valid_after': time_parameters.valid_from(date_format='human_readable'), "valid_after": time_parameters.valid_from(date_format="human_readable"),
'valid_before': time_parameters.valid_to(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], "critical_options": [
'extensions': [str(extension) for extension in self.extensions], str(critical_option) for critical_option in self.critical_options
'reserved': self.reserved, ],
'public_key': self.public_key, "extensions": [str(extension) for extension in self.extensions],
'signing_key': self.signing_key, "reserved": self.reserved,
"public_key": self.public_key,
"signing_key": self.signing_key,
} }
@@ -606,38 +658,46 @@ def apply_directives(directives):
raise ValueError("directives must be one of %s" % ", ".join(_DIRECTIVES)) raise ValueError("directives must be one of %s" % ", ".join(_DIRECTIVES))
directive_to_option = { directive_to_option = {
'no-x11-forwarding': OpensshCertificateOption('extension', 'permit-x11-forwarding', ''), "no-x11-forwarding": OpensshCertificateOption(
'no-agent-forwarding': OpensshCertificateOption('extension', 'permit-agent-forwarding', ''), "extension", "permit-x11-forwarding", ""
'no-port-forwarding': OpensshCertificateOption('extension', 'permit-port-forwarding', ''), ),
'no-pty': OpensshCertificateOption('extension', 'permit-pty', ''), "no-agent-forwarding": OpensshCertificateOption(
'no-user-rc': OpensshCertificateOption('extension', 'permit-user-rc', ''), "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: if "clear" in directives:
return [] return []
else: else:
return list(set(default_options()) - set(directive_to_option[d] for d in directives)) return list(
set(default_options()) - set(directive_to_option[d] for d in directives)
)
def default_options(): def default_options():
return [OpensshCertificateOption('extension', name, '') for name in _EXTENSIONS] return [OpensshCertificateOption("extension", name, "") for name in _EXTENSIONS]
def fingerprint(public_key): def fingerprint(public_key):
"""Generates a SHA256 hash and formats output to resemble ``ssh-keygen``""" """Generates a SHA256 hash and formats output to resemble ``ssh-keygen``"""
h = sha256() h = sha256()
h.update(public_key) h.update(public_key)
return b'SHA256:' + b64encode(h.digest()).rstrip(b'=') return b"SHA256:" + b64encode(h.digest()).rstrip(b"=")
def get_cert_info_object(key_type): def get_cert_info_object(key_type):
if key_type == 'rsa': if key_type == "rsa":
cert_info = OpensshRSACertificateInfo() cert_info = OpensshRSACertificateInfo()
elif key_type == 'dsa': elif key_type == "dsa":
cert_info = OpensshDSACertificateInfo() cert_info = OpensshDSACertificateInfo()
elif key_type in ('ecdsa-nistp256', 'ecdsa-nistp384', 'ecdsa-nistp521'): elif key_type in ("ecdsa-nistp256", "ecdsa-nistp384", "ecdsa-nistp521"):
cert_info = OpensshECDSACertificateInfo() cert_info = OpensshECDSACertificateInfo()
elif key_type == 'ed25519': elif key_type == "ed25519":
cert_info = OpensshED25519CertificateInfo() cert_info = OpensshED25519CertificateInfo()
else: else:
raise ValueError("%s is not a valid key type" % key_type) raise ValueError("%s is not a valid key type" % key_type)
@@ -647,12 +707,14 @@ def get_cert_info_object(key_type):
def get_option_type(name): def get_option_type(name):
if name in _CRITICAL_OPTIONS: if name in _CRITICAL_OPTIONS:
result = 'critical' result = "critical"
elif name in _EXTENSIONS: elif name in _EXTENSIONS:
result = 'extension' result = "extension"
else: else:
raise ValueError("%s is not a valid option. " % name + raise ValueError(
"Custom options must start with 'critical:' or 'extension:' to indicate type") "%s is not a valid option. " % name
+ "Custom options must start with 'critical:' or 'extension:' to indicate type"
)
return result return result
@@ -670,7 +732,7 @@ def parse_option_list(option_list):
directives.append(option.lower()) directives.append(option.lower())
else: else:
option_object = OpensshCertificateOption.from_string(option) option_object = OpensshCertificateOption.from_string(option)
if option_object.type == 'critical': if option_object.type == "critical":
critical_options.append(option_object) critical_options.append(option_object)
else: else:
extensions.append(option_object) extensions.append(option_object)

View File

@@ -5,22 +5,30 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import os import os
from base64 import b64encode, b64decode from base64 import b64decode, b64encode
from getpass import getuser from getpass import getuser
from socket import gethostname from socket import gethostname
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
try: try:
from cryptography import __version__ as CRYPTOGRAPHY_VERSION from cryptography import __version__ as CRYPTOGRAPHY_VERSION
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
from cryptography.hazmat.backends.openssl import backend from cryptography.hazmat.backends.openssl import backend
from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa, padding from cryptography.hazmat.primitives.asymmetric import dsa, ec, padding, rsa
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
Ed25519PublicKey,
)
if LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion("3.0"): if LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion("3.0"):
HAS_OPENSSH_PRIVATE_FORMAT = True HAS_OPENSSH_PRIVATE_FORMAT = True
@@ -30,41 +38,41 @@ try:
HAS_OPENSSH_SUPPORT = True HAS_OPENSSH_SUPPORT = True
_ALGORITHM_PARAMETERS = { _ALGORITHM_PARAMETERS = {
'rsa': { "rsa": {
'default_size': 2048, "default_size": 2048,
'valid_sizes': range(1024, 16384), "valid_sizes": range(1024, 16384),
'signer_params': { "signer_params": {
'padding': padding.PSS( "padding": padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH, salt_length=padding.PSS.MAX_LENGTH,
), ),
'algorithm': hashes.SHA256(), "algorithm": hashes.SHA256(),
}, },
}, },
'dsa': { "dsa": {
'default_size': 1024, "default_size": 1024,
'valid_sizes': [1024], "valid_sizes": [1024],
'signer_params': { "signer_params": {
'algorithm': hashes.SHA256(), "algorithm": hashes.SHA256(),
}, },
}, },
'ed25519': { "ed25519": {
'default_size': 256, "default_size": 256,
'valid_sizes': [256], "valid_sizes": [256],
'signer_params': {}, "signer_params": {},
}, },
'ecdsa': { "ecdsa": {
'default_size': 256, "default_size": 256,
'valid_sizes': [256, 384, 521], "valid_sizes": [256, 384, 521],
'signer_params': { "signer_params": {
'signature_algorithm': ec.ECDSA(hashes.SHA256()), "signature_algorithm": ec.ECDSA(hashes.SHA256()),
}, },
'curves': { "curves": {
256: ec.SECP256R1(), 256: ec.SECP256R1(),
384: ec.SECP384R1(), 384: ec.SECP384R1(),
521: ec.SECP521R1(), 521: ec.SECP521R1(),
} },
} },
} }
except ImportError: except ImportError:
HAS_OPENSSH_PRIVATE_FORMAT = False HAS_OPENSSH_PRIVATE_FORMAT = False
@@ -72,7 +80,7 @@ except ImportError:
CRYPTOGRAPHY_VERSION = "0.0" CRYPTOGRAPHY_VERSION = "0.0"
_ALGORITHM_PARAMETERS = {} _ALGORITHM_PARAMETERS = {}
_TEXT_ENCODING = 'UTF-8' _TEXT_ENCODING = "UTF-8"
class OpenSSHError(Exception): class OpenSSHError(Exception):
@@ -123,7 +131,7 @@ class AsymmetricKeypair(object):
"""Container for newly generated asymmetric key pairs or those loaded from existing files""" """Container for newly generated asymmetric key pairs or those loaded from existing files"""
@classmethod @classmethod
def generate(cls, keytype='rsa', size=None, passphrase=None): def generate(cls, keytype="rsa", size=None, passphrase=None):
"""Returns an Asymmetric_Keypair object generated with the supplied parameters """Returns an Asymmetric_Keypair object generated with the supplied parameters
or defaults to an unencrypted RSA-2048 key or defaults to an unencrypted RSA-2048 key
@@ -134,15 +142,14 @@ class AsymmetricKeypair(object):
if keytype not in _ALGORITHM_PARAMETERS.keys(): if keytype not in _ALGORITHM_PARAMETERS.keys():
raise InvalidKeyTypeError( raise InvalidKeyTypeError(
"%s is not a valid keytype. Valid keytypes are %s" % ( "%s is not a valid keytype. Valid keytypes are %s"
keytype, ", ".join(_ALGORITHM_PARAMETERS.keys()) % (keytype, ", ".join(_ALGORITHM_PARAMETERS.keys()))
)
) )
if not size: if not size:
size = _ALGORITHM_PARAMETERS[keytype]['default_size'] size = _ALGORITHM_PARAMETERS[keytype]["default_size"]
else: else:
if size not in _ALGORITHM_PARAMETERS[keytype]['valid_sizes']: if size not in _ALGORITHM_PARAMETERS[keytype]["valid_sizes"]:
raise InvalidKeySizeError( raise InvalidKeySizeError(
"%s is not a valid key size for %s keys" % (size, keytype) "%s is not a valid key size for %s keys" % (size, keytype)
) )
@@ -152,7 +159,7 @@ class AsymmetricKeypair(object):
else: else:
encryption_algorithm = serialization.NoEncryption() encryption_algorithm = serialization.NoEncryption()
if keytype == 'rsa': if keytype == "rsa":
privatekey = rsa.generate_private_key( privatekey = rsa.generate_private_key(
# Public exponent should always be 65537 to prevent issues # Public exponent should always be 65537 to prevent issues
# if improper padding is used during signing # if improper padding is used during signing
@@ -160,16 +167,16 @@ class AsymmetricKeypair(object):
key_size=size, key_size=size,
backend=backend, backend=backend,
) )
elif keytype == 'dsa': elif keytype == "dsa":
privatekey = dsa.generate_private_key( privatekey = dsa.generate_private_key(
key_size=size, key_size=size,
backend=backend, backend=backend,
) )
elif keytype == 'ed25519': elif keytype == "ed25519":
privatekey = Ed25519PrivateKey.generate() privatekey = Ed25519PrivateKey.generate()
elif keytype == 'ecdsa': elif keytype == "ecdsa":
privatekey = ec.generate_private_key( privatekey = ec.generate_private_key(
_ALGORITHM_PARAMETERS['ecdsa']['curves'][size], _ALGORITHM_PARAMETERS["ecdsa"]["curves"][size],
backend=backend, backend=backend,
) )
@@ -180,11 +187,18 @@ class AsymmetricKeypair(object):
size=size, size=size,
privatekey=privatekey, privatekey=privatekey,
publickey=publickey, publickey=publickey,
encryption_algorithm=encryption_algorithm encryption_algorithm=encryption_algorithm,
) )
@classmethod @classmethod
def load(cls, path, passphrase=None, private_key_format='PEM', public_key_format='PEM', no_public_key=False): 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 """Returns an Asymmetric_Keypair object loaded from the supplied file path
:path: A path to an existing private key to be loaded :path: A path to an existing private key to be loaded
@@ -203,31 +217,33 @@ class AsymmetricKeypair(object):
if no_public_key: if no_public_key:
publickey = privatekey.public_key() publickey = privatekey.public_key()
else: else:
publickey = load_publickey(path + '.pub', public_key_format) publickey = load_publickey(path + ".pub", public_key_format)
# Ed25519 keys are always of size 256 and do not have a key_size attribute # Ed25519 keys are always of size 256 and do not have a key_size attribute
if isinstance(privatekey, Ed25519PrivateKey): if isinstance(privatekey, Ed25519PrivateKey):
size = _ALGORITHM_PARAMETERS['ed25519']['default_size'] size = _ALGORITHM_PARAMETERS["ed25519"]["default_size"]
else: else:
size = privatekey.key_size size = privatekey.key_size
if isinstance(privatekey, rsa.RSAPrivateKey): if isinstance(privatekey, rsa.RSAPrivateKey):
keytype = 'rsa' keytype = "rsa"
elif isinstance(privatekey, dsa.DSAPrivateKey): elif isinstance(privatekey, dsa.DSAPrivateKey):
keytype = 'dsa' keytype = "dsa"
elif isinstance(privatekey, ec.EllipticCurvePrivateKey): elif isinstance(privatekey, ec.EllipticCurvePrivateKey):
keytype = 'ecdsa' keytype = "ecdsa"
elif isinstance(privatekey, Ed25519PrivateKey): elif isinstance(privatekey, Ed25519PrivateKey):
keytype = 'ed25519' keytype = "ed25519"
else: else:
raise InvalidKeyTypeError("Key type '%s' is not supported" % type(privatekey)) raise InvalidKeyTypeError(
"Key type '%s' is not supported" % type(privatekey)
)
return cls( return cls(
keytype=keytype, keytype=keytype,
size=size, size=size,
privatekey=privatekey, privatekey=privatekey,
publickey=publickey, publickey=publickey,
encryption_algorithm=encryption_algorithm encryption_algorithm=encryption_algorithm,
) )
def __init__(self, keytype, size, privatekey, publickey, encryption_algorithm): def __init__(self, keytype, size, privatekey, publickey, encryption_algorithm):
@@ -246,7 +262,7 @@ class AsymmetricKeypair(object):
self.__encryption_algorithm = encryption_algorithm self.__encryption_algorithm = encryption_algorithm
try: try:
self.verify(self.sign(b'message'), b'message') self.verify(self.sign(b"message"), b"message")
except InvalidSignatureError: except InvalidSignatureError:
raise InvalidPublicKeyFileError( raise InvalidPublicKeyFileError(
"The private key and public key of this keypair do not match" "The private key and public key of this keypair do not match"
@@ -256,8 +272,11 @@ class AsymmetricKeypair(object):
if not isinstance(other, AsymmetricKeypair): if not isinstance(other, AsymmetricKeypair):
return NotImplemented return NotImplemented
return (compare_publickeys(self.public_key, other.public_key) and return compare_publickeys(
compare_encryption_algorithms(self.encryption_algorithm, other.encryption_algorithm)) self.public_key, other.public_key
) and compare_encryption_algorithms(
self.encryption_algorithm, other.encryption_algorithm
)
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self == other
@@ -300,8 +319,7 @@ class AsymmetricKeypair(object):
try: try:
signature = self.__privatekey.sign( signature = self.__privatekey.sign(
data, data, **_ALGORITHM_PARAMETERS[self.__keytype]["signer_params"]
**_ALGORITHM_PARAMETERS[self.__keytype]['signer_params']
) )
except TypeError as e: except TypeError as e:
raise InvalidDataError(e) raise InvalidDataError(e)
@@ -319,7 +337,7 @@ class AsymmetricKeypair(object):
return self.__publickey.verify( return self.__publickey.verify(
signature, signature,
data, data,
**_ALGORITHM_PARAMETERS[self.__keytype]['signer_params'] **_ALGORITHM_PARAMETERS[self.__keytype]["signer_params"]
) )
except InvalidSignature: except InvalidSignature:
raise InvalidSignatureError raise InvalidSignatureError
@@ -340,7 +358,7 @@ class OpensshKeypair(object):
"""Container for OpenSSH encoded asymmetric key pairs""" """Container for OpenSSH encoded asymmetric key pairs"""
@classmethod @classmethod
def generate(cls, keytype='rsa', size=None, passphrase=None, comment=None): 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 """Returns an Openssh_Keypair object generated using the supplied parameters or defaults to a RSA-2048 key
:keytype: One of rsa, dsa, ecdsa, ed25519 :keytype: One of rsa, dsa, ecdsa, ed25519
@@ -353,7 +371,7 @@ class OpensshKeypair(object):
comment = "%s@%s" % (getuser(), gethostname()) comment = "%s@%s" % (getuser(), gethostname())
asym_keypair = AsymmetricKeypair.generate(keytype, size, passphrase) asym_keypair = AsymmetricKeypair.generate(keytype, size, passphrase)
openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, 'SSH') openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, "SSH")
openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment) openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment)
fingerprint = calculate_fingerprint(openssh_publickey) fingerprint = calculate_fingerprint(openssh_publickey)
@@ -377,10 +395,12 @@ class OpensshKeypair(object):
if no_public_key: if no_public_key:
comment = "" comment = ""
else: else:
comment = extract_comment(path + '.pub') comment = extract_comment(path + ".pub")
asym_keypair = AsymmetricKeypair.load(path, passphrase, 'SSH', 'SSH', no_public_key) asym_keypair = AsymmetricKeypair.load(
openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, 'SSH') 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) openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment)
fingerprint = calculate_fingerprint(openssh_publickey) fingerprint = calculate_fingerprint(openssh_publickey)
@@ -400,25 +420,29 @@ class OpensshKeypair(object):
:key_format: Format of the encoded private key. :key_format: Format of the encoded private key.
""" """
if key_format == 'SSH': if key_format == "SSH":
# Default to PEM format if SSH not available # Default to PEM format if SSH not available
if not HAS_OPENSSH_PRIVATE_FORMAT: if not HAS_OPENSSH_PRIVATE_FORMAT:
privatekey_format = serialization.PrivateFormat.PKCS8 privatekey_format = serialization.PrivateFormat.PKCS8
else: else:
privatekey_format = serialization.PrivateFormat.OpenSSH privatekey_format = serialization.PrivateFormat.OpenSSH
elif key_format == 'PKCS8': elif key_format == "PKCS8":
privatekey_format = serialization.PrivateFormat.PKCS8 privatekey_format = serialization.PrivateFormat.PKCS8
elif key_format == 'PKCS1': elif key_format == "PKCS1":
if asym_keypair.key_type == 'ed25519': if asym_keypair.key_type == "ed25519":
raise InvalidKeyFormatError("ed25519 keys cannot be represented in PKCS1 format") raise InvalidKeyFormatError(
"ed25519 keys cannot be represented in PKCS1 format"
)
privatekey_format = serialization.PrivateFormat.TraditionalOpenSSL privatekey_format = serialization.PrivateFormat.TraditionalOpenSSL
else: else:
raise InvalidKeyFormatError("The accepted private key formats are SSH, PKCS8, and PKCS1") raise InvalidKeyFormatError(
"The accepted private key formats are SSH, PKCS8, and PKCS1"
)
encoded_privatekey = asym_keypair.private_key.private_bytes( encoded_privatekey = asym_keypair.private_key.private_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=privatekey_format, format=privatekey_format,
encryption_algorithm=asym_keypair.encryption_algorithm encryption_algorithm=asym_keypair.encryption_algorithm,
) )
return encoded_privatekey return encoded_privatekey
@@ -437,11 +461,15 @@ class OpensshKeypair(object):
validate_comment(comment) validate_comment(comment)
encoded_publickey += (" %s" % comment).encode(encoding=_TEXT_ENCODING) if comment else b'' encoded_publickey += (
(" %s" % comment).encode(encoding=_TEXT_ENCODING) if comment else b""
)
return encoded_publickey return encoded_publickey
def __init__(self, asym_keypair, openssh_privatekey, openssh_publickey, fingerprint, comment): 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 :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 private key
@@ -460,7 +488,10 @@ class OpensshKeypair(object):
if not isinstance(other, OpensshKeypair): if not isinstance(other, OpensshKeypair):
return NotImplemented return NotImplemented
return self.asymmetric_keypair == other.asymmetric_keypair and self.comment == other.comment return (
self.asymmetric_keypair == other.asymmetric_keypair
and self.comment == other.comment
)
@property @property
def asymmetric_keypair(self): def asymmetric_keypair(self):
@@ -514,8 +545,14 @@ class OpensshKeypair(object):
validate_comment(comment) validate_comment(comment)
self.__comment = comment self.__comment = comment
encoded_comment = (" %s" % self.__comment).encode(encoding=_TEXT_ENCODING) if self.__comment else b'' encoded_comment = (
self.__openssh_publickey = b' '.join(self.__openssh_publickey.split(b' ', 2)[:2]) + 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 return self.__openssh_publickey
def update_passphrase(self, passphrase): def update_passphrase(self, passphrase):
@@ -525,36 +562,36 @@ class OpensshKeypair(object):
""" """
self.__asym_keypair.update_passphrase(passphrase) self.__asym_keypair.update_passphrase(passphrase)
self.__openssh_privatekey = OpensshKeypair.encode_openssh_privatekey(self.__asym_keypair, 'SSH') self.__openssh_privatekey = OpensshKeypair.encode_openssh_privatekey(
self.__asym_keypair, "SSH"
)
def load_privatekey(path, passphrase, key_format): def load_privatekey(path, passphrase, key_format):
privatekey_loaders = { privatekey_loaders = {
'PEM': serialization.load_pem_private_key, "PEM": serialization.load_pem_private_key,
'DER': serialization.load_der_private_key, "DER": serialization.load_der_private_key,
} }
# OpenSSH formatted private keys are not available in Cryptography <3.0 # OpenSSH formatted private keys are not available in Cryptography <3.0
if hasattr(serialization, 'load_ssh_private_key'): if hasattr(serialization, "load_ssh_private_key"):
privatekey_loaders['SSH'] = serialization.load_ssh_private_key privatekey_loaders["SSH"] = serialization.load_ssh_private_key
else: else:
privatekey_loaders['SSH'] = serialization.load_pem_private_key privatekey_loaders["SSH"] = serialization.load_pem_private_key
try: try:
privatekey_loader = privatekey_loaders[key_format] privatekey_loader = privatekey_loaders[key_format]
except KeyError: except KeyError:
raise InvalidKeyFormatError( raise InvalidKeyFormatError(
"%s is not a valid key format (%s)" % ( "%s is not a valid key format (%s)"
key_format, % (key_format, ",".join(privatekey_loaders.keys()))
','.join(privatekey_loaders.keys())
)
) )
if not os.path.exists(path): if not os.path.exists(path):
raise InvalidPrivateKeyFileError("No file was found at %s" % path) raise InvalidPrivateKeyFileError("No file was found at %s" % path)
try: try:
with open(path, 'rb') as f: with open(path, "rb") as f:
content = f.read() content = f.read()
privatekey = privatekey_loader( privatekey = privatekey_loader(
@@ -565,9 +602,9 @@ def load_privatekey(path, passphrase, key_format):
except ValueError as e: except ValueError as e:
# Revert to PEM if key could not be loaded in SSH format # Revert to PEM if key could not be loaded in SSH format
if key_format == 'SSH': if key_format == "SSH":
try: try:
privatekey = privatekey_loaders['PEM']( privatekey = privatekey_loaders["PEM"](
data=content, data=content,
password=passphrase, password=passphrase,
backend=backend, backend=backend,
@@ -590,26 +627,24 @@ def load_privatekey(path, passphrase, key_format):
def load_publickey(path, key_format): def load_publickey(path, key_format):
publickey_loaders = { publickey_loaders = {
'PEM': serialization.load_pem_public_key, "PEM": serialization.load_pem_public_key,
'DER': serialization.load_der_public_key, "DER": serialization.load_der_public_key,
'SSH': serialization.load_ssh_public_key, "SSH": serialization.load_ssh_public_key,
} }
try: try:
publickey_loader = publickey_loaders[key_format] publickey_loader = publickey_loaders[key_format]
except KeyError: except KeyError:
raise InvalidKeyFormatError( raise InvalidKeyFormatError(
"%s is not a valid key format (%s)" % ( "%s is not a valid key format (%s)"
key_format, % (key_format, ",".join(publickey_loaders.keys()))
','.join(publickey_loaders.keys())
)
) )
if not os.path.exists(path): if not os.path.exists(path):
raise InvalidPublicKeyFileError("No file was found at %s" % path) raise InvalidPublicKeyFileError("No file was found at %s" % path)
try: try:
with open(path, 'rb') as f: with open(path, "rb") as f:
content = f.read() content = f.read()
publickey = publickey_loader( publickey = publickey_loader(
@@ -638,10 +673,13 @@ def compare_publickeys(pk1, pk2):
def compare_encryption_algorithms(ea1, ea2): def compare_encryption_algorithms(ea1, ea2):
if isinstance(ea1, serialization.NoEncryption) and isinstance(ea2, serialization.NoEncryption): if isinstance(ea1, serialization.NoEncryption) and isinstance(
ea2, serialization.NoEncryption
):
return True return True
elif (isinstance(ea1, serialization.BestAvailableEncryption) and elif isinstance(ea1, serialization.BestAvailableEncryption) and isinstance(
isinstance(ea2, serialization.BestAvailableEncryption)): ea2, serialization.BestAvailableEncryption
):
return ea1.password == ea2.password return ea1.password == ea2.password
else: else:
return False return False
@@ -655,7 +693,7 @@ def get_encryption_algorithm(passphrase):
def validate_comment(comment): def validate_comment(comment):
if not hasattr(comment, 'encode'): if not hasattr(comment, "encode"):
raise InvalidCommentError("%s cannot be encoded to text" % comment) raise InvalidCommentError("%s cannot be encoded to text" % comment)
@@ -665,8 +703,8 @@ def extract_comment(path):
raise InvalidPublicKeyFileError("No file was found at %s" % path) raise InvalidPublicKeyFileError("No file was found at %s" % path)
try: try:
with open(path, 'rb') as f: with open(path, "rb") as f:
fields = f.read().split(b' ', 2) fields = f.read().split(b" ", 2)
if len(fields) == 3: if len(fields) == 3:
comment = fields[2].decode(_TEXT_ENCODING) comment = fields[2].decode(_TEXT_ENCODING)
else: else:
@@ -679,7 +717,9 @@ def extract_comment(path):
def calculate_fingerprint(openssh_publickey): def calculate_fingerprint(openssh_publickey):
digest = hashes.Hash(hashes.SHA256(), backend=backend) digest = hashes.Hash(hashes.SHA256(), backend=backend)
decoded_pubkey = b64decode(openssh_publickey.split(b' ')[1]) decoded_pubkey = b64decode(openssh_publickey.split(b" ")[1])
digest.update(decoded_pubkey) digest.update(decoded_pubkey)
return 'SHA256:%s' % b64encode(digest.finalize()).decode(encoding=_TEXT_ENCODING).rstrip('=') return "SHA256:%s" % b64encode(digest.finalize()).decode(
encoding=_TEXT_ENCODING
).rstrip("=")

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import os import os
@@ -15,6 +17,7 @@ from struct import Struct
from ansible.module_utils.six import PY3 from ansible.module_utils.six import PY3
# Protocol References # Protocol References
# ------------------- # -------------------
# https://datatracker.ietf.org/doc/html/rfc4251 # https://datatracker.ietf.org/doc/html/rfc4251
@@ -31,17 +34,17 @@ if PY3:
long = int long = int
# 0 (False) or 1 (True) encoded as a single byte # 0 (False) or 1 (True) encoded as a single byte
_BOOLEAN = Struct(b'?') _BOOLEAN = Struct(b"?")
# Unsigned 8-bit integer in network-byte-order # Unsigned 8-bit integer in network-byte-order
_UBYTE = Struct(b'!B') _UBYTE = Struct(b"!B")
_UBYTE_MAX = 0xFF _UBYTE_MAX = 0xFF
# Unsigned 32-bit integer in network-byte-order # Unsigned 32-bit integer in network-byte-order
_UINT32 = Struct(b'!I') _UINT32 = Struct(b"!I")
# Unsigned 32-bit little endian integer # Unsigned 32-bit little endian integer
_UINT32_LE = Struct(b'<I') _UINT32_LE = Struct(b"<I")
_UINT32_MAX = 0xFFFFFFFF _UINT32_MAX = 0xFFFFFFFF
# Unsigned 64-bit integer in network-byte-order # Unsigned 64-bit integer in network-byte-order
_UINT64 = Struct(b'!Q') _UINT64 = Struct(b"!Q")
_UINT64_MAX = 0xFFFFFFFFFFFFFFFF _UINT64_MAX = 0xFFFFFFFFFFFFFFFF
@@ -86,6 +89,7 @@ def secure_write(path, mode, content):
# See https://datatracker.ietf.org/doc/html/rfc4251#section-5 for SSH data types # See https://datatracker.ietf.org/doc/html/rfc4251#section-5 for SSH data types
class OpensshParser(object): class OpensshParser(object):
"""Parser for OpenSSH encoded objects""" """Parser for OpenSSH encoded objects"""
BOOLEAN_OFFSET = 1 BOOLEAN_OFFSET = 1
UINT32_OFFSET = 4 UINT32_OFFSET = 4
UINT64_OFFSET = 8 UINT64_OFFSET = 8
@@ -100,21 +104,21 @@ class OpensshParser(object):
def boolean(self): def boolean(self):
next_pos = self._check_position(self.BOOLEAN_OFFSET) next_pos = self._check_position(self.BOOLEAN_OFFSET)
value = _BOOLEAN.unpack(self._data[self._pos:next_pos])[0] value = _BOOLEAN.unpack(self._data[self._pos : next_pos])[0]
self._pos = next_pos self._pos = next_pos
return value return value
def uint32(self): def uint32(self):
next_pos = self._check_position(self.UINT32_OFFSET) next_pos = self._check_position(self.UINT32_OFFSET)
value = _UINT32.unpack(self._data[self._pos:next_pos])[0] value = _UINT32.unpack(self._data[self._pos : next_pos])[0]
self._pos = next_pos self._pos = next_pos
return value return value
def uint64(self): def uint64(self):
next_pos = self._check_position(self.UINT64_OFFSET) next_pos = self._check_position(self.UINT64_OFFSET)
value = _UINT64.unpack(self._data[self._pos:next_pos])[0] value = _UINT64.unpack(self._data[self._pos : next_pos])[0]
self._pos = next_pos self._pos = next_pos
return value return value
@@ -123,7 +127,7 @@ class OpensshParser(object):
next_pos = self._check_position(length) next_pos = self._check_position(length)
value = self._data[self._pos:next_pos] value = self._data[self._pos : next_pos]
self._pos = next_pos self._pos = next_pos
# Cast to bytes is required as a memoryview slice is itself a memoryview # Cast to bytes is required as a memoryview slice is itself a memoryview
return value if not PY3 else bytes(value) return value if not PY3 else bytes(value)
@@ -133,7 +137,7 @@ class OpensshParser(object):
def name_list(self): def name_list(self):
raw_string = self.string() raw_string = self.string()
return raw_string.decode('ASCII').split(',') return raw_string.decode("ASCII").split(",")
# Convenience function, but not an official data type from SSH # Convenience function, but not an official data type from SSH
def string_list(self): def string_list(self):
@@ -190,33 +194,39 @@ class OpensshParser(object):
signature_blob = parser.string() signature_blob = parser.string()
blob_parser = cls(signature_blob) blob_parser = cls(signature_blob)
if signature_type in (b'ssh-rsa', b'rsa-sha2-256', b'rsa-sha2-512'): 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/rfc4253#section-6.6
# https://datatracker.ietf.org/doc/html/rfc8332#section-3 # https://datatracker.ietf.org/doc/html/rfc8332#section-3
signature_data['s'] = cls._big_int(signature_blob, "big") signature_data["s"] = cls._big_int(signature_blob, "big")
elif signature_type == b'ssh-dss': elif signature_type == b"ssh-dss":
# https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 # https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
signature_data['r'] = cls._big_int(signature_blob[:20], "big") signature_data["r"] = cls._big_int(signature_blob[:20], "big")
signature_data['s'] = 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'): 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 # https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2
signature_data['r'] = blob_parser.mpint() signature_data["r"] = blob_parser.mpint()
signature_data['s'] = blob_parser.mpint() signature_data["s"] = blob_parser.mpint()
elif signature_type == b'ssh-ed25519': elif signature_type == b"ssh-ed25519":
# https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2 # https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2
signature_data['R'] = cls._big_int(signature_blob[:32], "little") signature_data["R"] = cls._big_int(signature_blob[:32], "little")
signature_data['S'] = cls._big_int(signature_blob[32:], "little") signature_data["S"] = cls._big_int(signature_blob[32:], "little")
else: else:
raise ValueError("%s is not a valid signature type" % signature_type) raise ValueError("%s is not a valid signature type" % signature_type)
signature_data['signature_type'] = signature_type signature_data["signature_type"] = signature_type
return signature_data return signature_data
@classmethod @classmethod
def _big_int(cls, raw_string, byte_order, signed=False): def _big_int(cls, raw_string, byte_order, signed=False):
if byte_order not in ("big", "little"): if byte_order not in ("big", "little"):
raise ValueError("Byte_order must be one of (big, little) not %s" % byte_order) raise ValueError(
"Byte_order must be one of (big, little) not %s" % byte_order
)
if PY3: if PY3:
return int.from_bytes(raw_string, byte_order, signed=signed) return int.from_bytes(raw_string, byte_order, signed=signed)
@@ -229,21 +239,31 @@ class OpensshParser(object):
msb = raw_string[0] if byte_order == "big" else raw_string[-1] msb = raw_string[0] if byte_order == "big" else raw_string[-1]
negative = bool(ord(msb) & 0x80) negative = bool(ord(msb) & 0x80)
# Match pad value for two's complement # Match pad value for two's complement
pad = b'\xFF' if signed and negative else b'\x00' 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 # The definition of ``mpint`` enforces that unnecessary bytes are not encoded so they are added back
pad_length = (4 - byte_length % 4) pad_length = 4 - byte_length % 4
if pad_length < 4: if pad_length < 4:
raw_string = pad * pad_length + raw_string if byte_order == "big" else raw_string + pad * pad_length raw_string = (
pad * pad_length + raw_string
if byte_order == "big"
else raw_string + pad * pad_length
)
byte_length += pad_length byte_length += pad_length
# Accumulate arbitrary precision integer bytes in the appropriate order # Accumulate arbitrary precision integer bytes in the appropriate order
if byte_order == "big": if byte_order == "big":
for i in range(0, byte_length, cls.UINT32_OFFSET): for i in range(0, byte_length, cls.UINT32_OFFSET):
left_shift = result << cls.UINT32_OFFSET * 8 left_shift = result << cls.UINT32_OFFSET * 8
result = left_shift + _UINT32.unpack(raw_string[i:i + cls.UINT32_OFFSET])[0] result = (
left_shift
+ _UINT32.unpack(raw_string[i : i + cls.UINT32_OFFSET])[0]
)
else: else:
for i in range(byte_length, 0, -cls.UINT32_OFFSET): for i in range(byte_length, 0, -cls.UINT32_OFFSET):
left_shift = result << cls.UINT32_OFFSET * 8 left_shift = result << cls.UINT32_OFFSET * 8
result = left_shift + _UINT32_LE.unpack(raw_string[i - cls.UINT32_OFFSET:i])[0] result = (
left_shift
+ _UINT32_LE.unpack(raw_string[i - cls.UINT32_OFFSET : i])[0]
)
# Adjust for two's complement # Adjust for two's complement
if signed and negative: if signed and negative:
result -= 1 << (8 * byte_length) result -= 1 << (8 * byte_length)
@@ -259,10 +279,13 @@ class _OpensshWriter(object):
It is not to be used to construct Openssh objects, but rather as a utility to assist It is not to be used to construct Openssh objects, but rather as a utility to assist
in validating parsed material. in validating parsed material.
""" """
def __init__(self, buffer=None): def __init__(self, buffer=None):
if buffer is not None: if buffer is not None:
if not isinstance(buffer, (bytes, bytearray)): if not isinstance(buffer, (bytes, bytearray)):
raise TypeError("Buffer must be a bytes-like object not %s" % type(buffer)) raise TypeError(
"Buffer must be a bytes-like object not %s" % type(buffer)
)
else: else:
buffer = bytearray() buffer = bytearray()
@@ -280,7 +303,9 @@ class _OpensshWriter(object):
if not isinstance(value, int): if not isinstance(value, int):
raise TypeError("Value must be of type int not %s" % type(value)) raise TypeError("Value must be of type int not %s" % type(value))
if value < 0 or value > _UINT32_MAX: if value < 0 or value > _UINT32_MAX:
raise ValueError("Value must be a positive integer less than %s" % _UINT32_MAX) raise ValueError(
"Value must be a positive integer less than %s" % _UINT32_MAX
)
self._buff.extend(_UINT32.pack(value)) self._buff.extend(_UINT32.pack(value))
@@ -290,7 +315,9 @@ class _OpensshWriter(object):
if not isinstance(value, (long, int)): if not isinstance(value, (long, int)):
raise TypeError("Value must be of type (long, int) not %s" % type(value)) raise TypeError("Value must be of type (long, int) not %s" % type(value))
if value < 0 or value > _UINT64_MAX: if value < 0 or value > _UINT64_MAX:
raise ValueError("Value must be a positive integer less than %s" % _UINT64_MAX) raise ValueError(
"Value must be a positive integer less than %s" % _UINT64_MAX
)
self._buff.extend(_UINT64.pack(value)) self._buff.extend(_UINT64.pack(value))
@@ -317,7 +344,7 @@ class _OpensshWriter(object):
raise TypeError("Value must be a list of byte strings not %s" % type(value)) raise TypeError("Value must be a list of byte strings not %s" % type(value))
try: try:
self.string(','.join(value).encode('ASCII')) self.string(",".join(value).encode("ASCII"))
except UnicodeEncodeError as e: except UnicodeEncodeError as e:
raise ValueError("Name-list's must consist of US-ASCII characters: %s" % e) raise ValueError("Name-list's must consist of US-ASCII characters: %s" % e)
@@ -362,9 +389,9 @@ class _OpensshWriter(object):
result = bytes() result = bytes()
# 0 and -1 are treated as special cases since they are used as sentinels for all other values # 0 and -1 are treated as special cases since they are used as sentinels for all other values
if num == 0: if num == 0:
result += b'\x00' result += b"\x00"
elif num == -1: elif num == -1:
result += b'\xFF' result += b"\xff"
elif num > 0: elif num > 0:
while num >> 32: while num >> 32:
result = _UINT32.pack(num & _UINT32_MAX) + result result = _UINT32.pack(num & _UINT32_MAX) + result
@@ -375,7 +402,7 @@ class _OpensshWriter(object):
num = num >> 8 num = num >> 8
# Zero pad final byte if most-significant bit is 1 as per mpint definition # Zero pad final byte if most-significant bit is 1 as per mpint definition
if ord(result[0]) & 0x80: if ord(result[0]) & 0x80:
result = b'\x00' + result result = b"\x00" + result
else: else:
while (num >> 32) < -1: while (num >> 32) < -1:
result = _UINT32.pack(num & _UINT32_MAX) + result result = _UINT32.pack(num & _UINT32_MAX) + result
@@ -384,7 +411,7 @@ class _OpensshWriter(object):
result = _UBYTE.pack(num & _UBYTE_MAX) + result result = _UBYTE.pack(num & _UBYTE_MAX) + result
num = num >> 8 num = num >> 8
if not ord(result[0]) & 0x80: if not ord(result[0]) & 0x80:
result = b'\xFF' + result result = b"\xff" + result
return result return result

View File

@@ -5,10 +5,11 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
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_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
convert_int_to_hex, convert_int_to_hex,
) )
@@ -20,12 +21,12 @@ def th(number):
mod_100 = abs_number % 100 mod_100 = abs_number % 100
if mod_100 not in (11, 12, 13): if mod_100 not in (11, 12, 13):
if mod_10 == 1: if mod_10 == 1:
return 'st' return "st"
if mod_10 == 2: if mod_10 == 2:
return 'nd' return "nd"
if mod_10 == 3: if mod_10 == 3:
return 'rd' return "rd"
return 'th' return "th"
def parse_serial(value): def parse_serial(value):
@@ -34,14 +35,17 @@ def parse_serial(value):
""" """
value = to_native(value) value = to_native(value)
result = 0 result = 0
for i, part in enumerate(value.split(':')): for i, part in enumerate(value.split(":")):
try: try:
part_value = int(part, 16) part_value = int(part, 16)
if part_value < 0 or part_value > 255: if part_value < 0 or part_value > 255:
raise ValueError('the value is not in range [0, 255]') raise ValueError("the value is not in range [0, 255]")
except ValueError as exc: except ValueError as exc:
raise ValueError("The {idx}{th} part {part!r} is not a hexadecimal number in range [0, 255]: {exc}".format( raise ValueError(
idx=i + 1, th=th(i + 1), part=part, exc=exc)) "The {idx}{th} part {part!r} is not a hexadecimal number in range [0, 255]: {exc}".format(
idx=i + 1, th=th(i + 1), part=part, exc=exc
)
)
result = (result << 8) | part_value result = (result << 8) | part_value
return result return result
@@ -52,5 +56,5 @@ def to_serial(value):
""" """
value = convert_int_to_hex(value).upper() value = convert_int_to_hex(value).upper()
if len(value) % 2 != 0: if len(value) % 2 != 0:
value = '0' + value value = "0" + value
return ':'.join(value[i:i + 2] for i in range(0, len(value), 2)) return ":".join(value[i : i + 2] for i in range(0, len(value), 2))

View File

@@ -5,6 +5,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -13,7 +15,6 @@ import re
import sys import sys
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters 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,
) )
@@ -32,13 +33,13 @@ except AttributeError:
return _DURATION_ZERO return _DURATION_ZERO
def tzname(self, dt): def tzname(self, dt):
return 'UTC' return "UTC"
def fromutc(self, dt): def fromutc(self, dt):
return dt return dt
def __repr__(self): def __repr__(self):
return 'UTC' return "UTC"
UTC = _UTCClass() UTC = _UTCClass()
@@ -68,20 +69,29 @@ def remove_timezone(timestamp):
def add_or_remove_timezone(timestamp, with_timezone): def add_or_remove_timezone(timestamp, with_timezone):
return ensure_utc_timezone(timestamp) if with_timezone else remove_timezone(timestamp) return (
ensure_utc_timezone(timestamp) if with_timezone else remove_timezone(timestamp)
)
if sys.version_info < (3, 3): if sys.version_info < (3, 3):
def get_epoch_seconds(timestamp): def get_epoch_seconds(timestamp):
epoch = datetime.datetime(1970, 1, 1, tzinfo=UTC if timestamp.tzinfo is not None else None) epoch = datetime.datetime(
1970, 1, 1, tzinfo=UTC if timestamp.tzinfo is not None else None
)
delta = timestamp - epoch delta = timestamp - epoch
try: try:
return delta.total_seconds() return delta.total_seconds()
except AttributeError: except AttributeError:
# Python 2.6 and earlier: total_seconds() does not yet exist, so we use the formula from # Python 2.6 and earlier: total_seconds() does not yet exist, so we use the formula from
# https://docs.python.org/2/library/datetime.html#datetime.timedelta.total_seconds # https://docs.python.org/2/library/datetime.html#datetime.timedelta.total_seconds
return (delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10**6) / 10**6 return (
delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10**6
) / 10**6
else: else:
def get_epoch_seconds(timestamp): def get_epoch_seconds(timestamp):
if timestamp.tzinfo is None: if timestamp.tzinfo is None:
# timestamp.timestamp() is offset by the local timezone if timestamp has no timezone # timestamp.timestamp() is offset by the local timezone if timestamp has no timezone
@@ -100,7 +110,8 @@ def convert_relative_to_datetime(relative_time_string, with_timezone=False, now=
parsed_result = re.match( parsed_result = re.match(
r"^(?P<prefix>[+-])((?P<weeks>\d+)[wW])?((?P<days>\d+)[dD])?((?P<hours>\d+)[hH])?((?P<minutes>\d+)[mM])?((?P<seconds>\d+)[sS]?)?$", r"^(?P<prefix>[+-])((?P<weeks>\d+)[wW])?((?P<days>\d+)[dD])?((?P<hours>\d+)[hH])?((?P<minutes>\d+)[mM])?((?P<seconds>\d+)[sS]?)?$",
relative_time_string) relative_time_string,
)
if parsed_result is None or len(relative_time_string) == 1: if parsed_result is None or len(relative_time_string) == 1:
# not matched or only a single "+" or "-" # not matched or only a single "+" or "-"
@@ -114,11 +125,9 @@ def convert_relative_to_datetime(relative_time_string, with_timezone=False, now=
if parsed_result.group("hours") is not None: if parsed_result.group("hours") is not None:
offset += datetime.timedelta(hours=int(parsed_result.group("hours"))) offset += datetime.timedelta(hours=int(parsed_result.group("hours")))
if parsed_result.group("minutes") is not None: if parsed_result.group("minutes") is not None:
offset += datetime.timedelta( offset += datetime.timedelta(minutes=int(parsed_result.group("minutes")))
minutes=int(parsed_result.group("minutes")))
if parsed_result.group("seconds") is not None: if parsed_result.group("seconds") is not None:
offset += datetime.timedelta( offset += datetime.timedelta(seconds=int(parsed_result.group("seconds")))
seconds=int(parsed_result.group("seconds")))
if now is None: if now is None:
now = get_now_datetime(with_timezone=with_timezone) now = get_now_datetime(with_timezone=with_timezone)
@@ -131,7 +140,9 @@ def convert_relative_to_datetime(relative_time_string, with_timezone=False, now=
return now - offset return now - offset
def get_relative_time_option(input_string, input_name, backend='cryptography', with_timezone=False, now=None): def get_relative_time_option(
input_string, input_name, backend="cryptography", with_timezone=False, now=None
):
"""Return an absolute timespec if a relative timespec or an ASN1 formatted """Return an absolute timespec if a relative timespec or an ASN1 formatted
string is provided. string is provided.
@@ -140,24 +151,32 @@ def get_relative_time_option(input_string, input_name, backend='cryptography', w
result = to_native(input_string) result = to_native(input_string)
if result is None: if result is None:
raise OpenSSLObjectError( raise OpenSSLObjectError(
'The timespec "%s" for %s is not valid' % 'The timespec "%s" for %s is not valid' % input_string, input_name
input_string, input_name) )
# Relative time # Relative time
if result.startswith("+") or result.startswith("-"): if result.startswith("+") or result.startswith("-"):
result_datetime = convert_relative_to_datetime(result, with_timezone=with_timezone, now=now) result_datetime = convert_relative_to_datetime(
if backend == 'pyopenssl': result, with_timezone=with_timezone, now=now
)
if backend == "pyopenssl":
return result_datetime.strftime("%Y%m%d%H%M%SZ") return result_datetime.strftime("%Y%m%d%H%M%SZ")
elif backend == 'cryptography': elif backend == "cryptography":
return result_datetime return result_datetime
# Absolute time # Absolute time
if backend == 'pyopenssl': if backend == "pyopenssl":
return input_string return input_string
elif backend == 'cryptography': elif backend == "cryptography":
for date_fmt, length in [ for date_fmt, length in [
('%Y%m%d%H%M%SZ', 15), # this also parses '202401020304Z', but as datetime(2024, 1, 2, 3, 0, 4) (
('%Y%m%d%H%MZ', 13), "%Y%m%d%H%M%SZ",
('%Y%m%d%H%M%S%z', 14 + 5), # this also parses '202401020304+0000', but as datetime(2024, 1, 2, 3, 0, 4, tzinfo=...) 15,
('%Y%m%d%H%M%z', 12 + 5), ), # this also parses '202401020304Z', but as datetime(2024, 1, 2, 3, 0, 4)
("%Y%m%d%H%MZ", 13),
(
"%Y%m%d%H%M%S%z",
14 + 5,
), # this also parses '202401020304+0000', but as datetime(2024, 1, 2, 3, 0, 4, tzinfo=...)
("%Y%m%d%H%M%z", 12 + 5),
]: ]:
if len(result) != length: if len(result) != length:
continue continue
@@ -169,6 +188,5 @@ def get_relative_time_option(input_string, input_name, backend='cryptography', w
return add_or_remove_timezone(res, with_timezone=with_timezone) return add_or_remove_timezone(res, with_timezone=with_timezone)
raise OpenSSLObjectError( raise OpenSSLObjectError(
'The time spec "%s" for %s is invalid' % 'The time spec "%s" for %s is invalid' % (input_string, input_name)
(input_string, input_name)
) )

View File

@@ -7,6 +7,8 @@
"""Provide version object to compare version numbers.""" """Provide version object to compare version numbers."""
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -103,8 +105,8 @@ options:
external_account_binding: external_account_binding:
description: description:
- Allows to provide external account binding data during account creation. - Allows to provide external account binding data during account creation.
- This is used by CAs like Sectigo to bind a new ACME account to an existing CA-specific account, to be able to properly - This is used by CAs like Sectigo, HARICA, or ZeroSSL to bind a new ACME account to an existing CA-specific account,
identify a customer. to be able to properly identify a customer.
- Only used when creating a new account. Can not be specified for ACME v1. - Only used when creating a new account. Can not be specified for ACME v1.
type: dict type: dict
suboptions: suboptions:
@@ -130,6 +132,7 @@ options:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Make sure account exists and has given contacts. We agree to TOS. - name: Make sure account exists and has given contacts. We agree to TOS.
community.crypto.acme_account: community.crypto.acme_account:
account_key_src: /etc/pki/cert/private/account.key account_key_src: /etc/pki/cert/private/account.key
@@ -168,92 +171,105 @@ account_uri:
import base64 import base64
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
create_default_argspec,
ACMEClient,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.account import ( from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
ACMEAccount, ACMEAccount,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
ACMEClient,
create_backend,
create_default_argspec,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ModuleFailException,
KeyParsingError, KeyParsingError,
ModuleFailException,
) )
def main(): def main():
argument_spec = create_default_argspec() argument_spec = create_default_argspec()
argument_spec.update_argspec( argument_spec.update_argspec(
terms_agreed=dict(type='bool', default=False), terms_agreed=dict(type="bool", default=False),
state=dict(type='str', required=True, choices=['absent', 'present', 'changed_key']), state=dict(
allow_creation=dict(type='bool', default=True), type="str", required=True, choices=["absent", "present", "changed_key"]
contact=dict(type='list', elements='str', default=[]), ),
new_account_key_src=dict(type='path'), allow_creation=dict(type="bool", default=True),
new_account_key_content=dict(type='str', no_log=True), contact=dict(type="list", elements="str", default=[]),
new_account_key_passphrase=dict(type='str', no_log=True), new_account_key_src=dict(type="path"),
external_account_binding=dict(type='dict', options=dict( new_account_key_content=dict(type="str", no_log=True),
kid=dict(type='str', required=True), new_account_key_passphrase=dict(type="str", no_log=True),
alg=dict(type='str', required=True, choices=['HS256', 'HS384', 'HS512']), external_account_binding=dict(
key=dict(type='str', required=True, no_log=True), type="dict",
)) options=dict(
kid=dict(type="str", required=True),
alg=dict(
type="str", required=True, choices=["HS256", "HS384", "HS512"]
),
key=dict(type="str", required=True, no_log=True),
),
),
) )
argument_spec.update( argument_spec.update(
mutually_exclusive=( mutually_exclusive=(["new_account_key_src", "new_account_key_content"],),
['new_account_key_src', 'new_account_key_content'],
),
required_if=( required_if=(
# Make sure that for state == changed_key, one of # Make sure that for state == changed_key, one of
# new_account_key_src and new_account_key_content are specified # new_account_key_src and new_account_key_content are specified
['state', 'changed_key', ['new_account_key_src', 'new_account_key_content'], True], [
"state",
"changed_key",
["new_account_key_src", "new_account_key_content"],
True,
],
), ),
) )
module = argument_spec.create_ansible_module(supports_check_mode=True) module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True) backend = create_backend(module, True)
if module.params['external_account_binding']: if module.params["external_account_binding"]:
# Make sure padding is there # Make sure padding is there
key = module.params['external_account_binding']['key'] key = module.params["external_account_binding"]["key"]
if len(key) % 4 != 0: if len(key) % 4 != 0:
key = key + ('=' * (4 - (len(key) % 4))) key = key + ("=" * (4 - (len(key) % 4)))
# Make sure key is Base64 encoded # Make sure key is Base64 encoded
try: try:
base64.urlsafe_b64decode(key) base64.urlsafe_b64decode(key)
except Exception as e: except Exception as e:
module.fail_json(msg='Key for external_account_binding must be Base64 URL encoded (%s)' % e) module.fail_json(
module.params['external_account_binding']['key'] = key msg="Key for external_account_binding must be Base64 URL encoded (%s)"
% e
)
module.params["external_account_binding"]["key"] = key
try: try:
client = ACMEClient(module, backend) client = ACMEClient(module, backend)
account = ACMEAccount(client) account = ACMEAccount(client)
changed = False changed = False
state = module.params.get('state') state = module.params.get("state")
diff_before = {} diff_before = {}
diff_after = {} diff_after = {}
if state == 'absent': if state == "absent":
created, account_data = account.setup_account(allow_creation=False) created, account_data = account.setup_account(allow_creation=False)
if account_data: if account_data:
diff_before = dict(account_data) diff_before = dict(account_data)
diff_before['public_account_key'] = client.account_key_data['jwk'] diff_before["public_account_key"] = client.account_key_data["jwk"]
if created: if created:
raise AssertionError('Unwanted account creation') raise AssertionError("Unwanted account creation")
if account_data is not None: if account_data is not None:
# Account is not yet deactivated # Account is not yet deactivated
if not module.check_mode: if not module.check_mode:
# Deactivate it # Deactivate it
payload = { payload = {"status": "deactivated"}
'status': 'deactivated'
}
result, info = client.send_signed_request( result, info = client.send_signed_request(
client.account_uri, payload, error_msg='Failed to deactivate account', expected_status_codes=[200]) client.account_uri,
payload,
error_msg="Failed to deactivate account",
expected_status_codes=[200],
)
changed = True changed = True
elif state == 'present': elif state == "present":
allow_creation = module.params.get('allow_creation') allow_creation = module.params.get("allow_creation")
contact = [str(v) for v in module.params.get('contact')] contact = [str(v) for v in module.params.get("contact")]
terms_agreed = module.params.get('terms_agreed') terms_agreed = module.params.get("terms_agreed")
external_account_binding = module.params.get('external_account_binding') external_account_binding = module.params.get("external_account_binding")
created, account_data = account.setup_account( created, account_data = account.setup_account(
contact, contact,
terms_agreed=terms_agreed, terms_agreed=terms_agreed,
@@ -261,77 +277,87 @@ def main():
external_account_binding=external_account_binding, external_account_binding=external_account_binding,
) )
if account_data is None: if account_data is None:
raise ModuleFailException(msg='Account does not exist or is deactivated.') raise ModuleFailException(
msg="Account does not exist or is deactivated."
)
if created: if created:
diff_before = {} diff_before = {}
else: else:
diff_before = dict(account_data) diff_before = dict(account_data)
diff_before['public_account_key'] = client.account_key_data['jwk'] diff_before["public_account_key"] = client.account_key_data["jwk"]
updated = False updated = False
if not created: if not created:
updated, account_data = account.update_account(account_data, contact) updated, account_data = account.update_account(account_data, contact)
changed = created or updated changed = created or updated
diff_after = dict(account_data) diff_after = dict(account_data)
diff_after['public_account_key'] = client.account_key_data['jwk'] diff_after["public_account_key"] = client.account_key_data["jwk"]
elif state == 'changed_key': elif state == "changed_key":
# Parse new account key # Parse new account key
try: try:
new_key_data = client.parse_key( new_key_data = client.parse_key(
module.params.get('new_account_key_src'), module.params.get("new_account_key_src"),
module.params.get('new_account_key_content'), module.params.get("new_account_key_content"),
passphrase=module.params.get('new_account_key_passphrase'), passphrase=module.params.get("new_account_key_passphrase"),
) )
except KeyParsingError as e: except KeyParsingError as e:
raise ModuleFailException("Error while parsing new account key: {msg}".format(msg=e.msg)) raise ModuleFailException(
"Error while parsing new account key: {msg}".format(msg=e.msg)
)
# Verify that the account exists and has not been deactivated # Verify that the account exists and has not been deactivated
created, account_data = account.setup_account(allow_creation=False) created, account_data = account.setup_account(allow_creation=False)
if created: if created:
raise AssertionError('Unwanted account creation') raise AssertionError("Unwanted account creation")
if account_data is None: if account_data is None:
raise ModuleFailException(msg='Account does not exist or is deactivated.') raise ModuleFailException(
msg="Account does not exist or is deactivated."
)
diff_before = dict(account_data) diff_before = dict(account_data)
diff_before['public_account_key'] = client.account_key_data['jwk'] diff_before["public_account_key"] = client.account_key_data["jwk"]
# Now we can start the account key rollover # Now we can start the account key rollover
if not module.check_mode: if not module.check_mode:
# Compose inner signed message # Compose inner signed message
# https://tools.ietf.org/html/rfc8555#section-7.3.5 # https://tools.ietf.org/html/rfc8555#section-7.3.5
url = client.directory['keyChange'] url = client.directory["keyChange"]
protected = { protected = {
"alg": new_key_data['alg'], "alg": new_key_data["alg"],
"jwk": new_key_data['jwk'], "jwk": new_key_data["jwk"],
"url": url, "url": url,
} }
payload = { payload = {
"account": client.account_uri, "account": client.account_uri,
"newKey": new_key_data['jwk'], # specified in draft 12 and older "newKey": new_key_data["jwk"], # specified in draft 12 and older
"oldKey": client.account_jwk, # specified in draft 13 and newer "oldKey": client.account_jwk, # specified in draft 13 and newer
} }
data = client.sign_request(protected, payload, new_key_data) data = client.sign_request(protected, payload, new_key_data)
# Send request and verify result # Send request and verify result
result, info = client.send_signed_request( result, info = client.send_signed_request(
url, data, error_msg='Failed to rollover account key', expected_status_codes=[200]) url,
data,
error_msg="Failed to rollover account key",
expected_status_codes=[200],
)
if module._diff: if module._diff:
client.account_key_data = new_key_data client.account_key_data = new_key_data
client.account_jws_header['alg'] = new_key_data['alg'] client.account_jws_header["alg"] = new_key_data["alg"]
diff_after = account.get_account_data() diff_after = account.get_account_data()
elif module._diff: elif module._diff:
# Kind of fake diff_after # Kind of fake diff_after
diff_after = dict(diff_before) diff_after = dict(diff_before)
diff_after['public_account_key'] = new_key_data['jwk'] diff_after["public_account_key"] = new_key_data["jwk"]
changed = True changed = True
result = { result = {
'changed': changed, "changed": changed,
'account_uri': client.account_uri, "account_uri": client.account_uri,
} }
if module._diff: if module._diff:
result['diff'] = { result["diff"] = {
'before': diff_before, "before": diff_before,
'after': diff_after, "after": diff_after,
} }
module.exit_json(**result) module.exit_json(**result)
except ModuleFailException as e: except ModuleFailException as e:
e.do_fail(module) e.do_fail(module)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -47,6 +49,7 @@ seealso:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Check whether an account with the given account key exists - name: Check whether an account with the given account key exists
community.crypto.acme_account_info: community.crypto.acme_account_info:
account_key_src: /etc/pki/cert/private/account.key account_key_src: /etc/pki/cert/private/account.key
@@ -206,42 +209,47 @@ order_uris:
version_added: 1.5.0 version_added: 1.5.0
""" """
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
create_default_argspec,
ACMEClient,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.account import ( from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
ACMEAccount, ACMEAccount,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException ACMEClient,
create_backend,
create_default_argspec,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ModuleFailException,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
process_links, process_links,
) )
def get_orders_list(module, client, orders_url): def get_orders_list(module, client, orders_url):
''' """
Retrieves orders list (handles pagination). Retrieves orders list (handles pagination).
''' """
orders = [] orders = []
while orders_url: while orders_url:
# Get part of orders list # Get part of orders list
res, info = client.get_request(orders_url, parse_json_result=True, fail_on_error=True) res, info = client.get_request(
if not res.get('orders'): orders_url, parse_json_result=True, fail_on_error=True
)
if not res.get("orders"):
if orders: if orders:
module.warn('When retrieving orders list part {0}, got empty result list'.format(orders_url)) module.warn(
"When retrieving orders list part {0}, got empty result list".format(
orders_url
)
)
break break
# Add order URLs to result list # Add order URLs to result list
orders.extend(res['orders']) orders.extend(res["orders"])
# Extract URL of next part of results list # Extract URL of next part of results list
new_orders_url = [] new_orders_url = []
def f(link, relation): def f(link, relation):
if relation == 'next': if relation == "next":
new_orders_url.append(link) new_orders_url.append(link)
process_links(info, f) process_links(info, f)
@@ -254,16 +262,18 @@ def get_orders_list(module, client, orders_url):
def get_order(client, order_url): def get_order(client, order_url):
''' """
Retrieve order data. Retrieve order data.
''' """
return client.get_request(order_url, parse_json_result=True, fail_on_error=True)[0] return client.get_request(order_url, parse_json_result=True, fail_on_error=True)[0]
def main(): def main():
argument_spec = create_default_argspec() argument_spec = create_default_argspec()
argument_spec.update_argspec( argument_spec.update_argspec(
retrieve_orders=dict(type='str', default='ignore', choices=['ignore', 'url_list', 'object_list']), retrieve_orders=dict(
type="str", default="ignore", choices=["ignore", "url_list", "object_list"]
),
) )
module = argument_spec.create_ansible_module(supports_check_mode=True) module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True) backend = create_backend(module, True)
@@ -278,28 +288,31 @@ def main():
remove_account_uri_if_not_exists=True, remove_account_uri_if_not_exists=True,
) )
if created: if created:
raise AssertionError('Unwanted account creation') raise AssertionError("Unwanted account creation")
result = { result = {
'changed': False, "changed": False,
'exists': client.account_uri is not None, "exists": client.account_uri is not None,
'account_uri': client.account_uri, "account_uri": client.account_uri,
} }
if client.account_uri is not None: if client.account_uri is not None:
# Make sure promised data is there # Make sure promised data is there
if 'contact' not in account_data: if "contact" not in account_data:
account_data['contact'] = [] account_data["contact"] = []
account_data['public_account_key'] = client.account_key_data['jwk'] account_data["public_account_key"] = client.account_key_data["jwk"]
result['account'] = account_data result["account"] = account_data
# Retrieve orders list # Retrieve orders list
if account_data.get('orders') and module.params['retrieve_orders'] != 'ignore': if (
orders = get_orders_list(module, client, account_data['orders']) account_data.get("orders")
result['order_uris'] = orders and module.params["retrieve_orders"] != "ignore"
if module.params['retrieve_orders'] == 'object_list': ):
result['orders'] = [get_order(client, order) for order in orders] orders = get_orders_list(module, client, account_data["orders"])
result["order_uris"] = orders
if module.params["retrieve_orders"] == "object_list":
result["orders"] = [get_order(client, order) for order in orders]
module.exit_json(**result) module.exit_json(**result)
except ModuleFailException as e: except ModuleFailException as e:
e.do_fail(module) e.do_fail(module)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -17,8 +19,7 @@ short_description: Retrieves ACME Renewal Information (ARI) for a certificate
description: description:
- Allows to retrieve renewal information on a certificate obtained with the L(ACME protocol,https://tools.ietf.org/html/rfc8555). - Allows to retrieve renewal information on a certificate obtained with the L(ACME protocol,https://tools.ietf.org/html/rfc8555).
- This module only works with the ACME v2 protocol, and requires the ACME server to support the ARI extension - This module only works with the ACME v2 protocol, and requires the ACME server to support the ARI extension
(U(https://datatracker.ietf.org/doc/draft-ietf-acme-ari/)). (L(RFC 9773, https://www.rfc-editor.org/rfc/rfc9773.html)).
This module implements version 3 of the ARI draft.
extends_documentation_fragment: extends_documentation_fragment:
- community.crypto.acme.basic - community.crypto.acme.basic
- community.crypto.acme.no_account - community.crypto.acme.no_account
@@ -44,6 +45,7 @@ seealso:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
- name: Retrieve renewal information for a certificate - name: Retrieve renewal information for a certificate
community.crypto.acme_ari_info: community.crypto.acme_ari_info:
certificate_path: /etc/httpd/ssl/sample.com.crt certificate_path: /etc/httpd/ssl/sample.com.crt
@@ -56,7 +58,7 @@ EXAMPLES = r"""
RETURN = r""" RETURN = r"""
renewal_info: renewal_info:
description: The ARI renewal info object (U(https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-4.2)). description: The ARI renewal info object (U(https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2)).
returned: success returned: success
type: dict type: dict
contains: contains:
@@ -97,27 +99,24 @@ renewal_info:
""" """
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
ACMEClient,
create_backend, create_backend,
create_default_argspec, create_default_argspec,
ACMEClient,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException ModuleFailException,
)
def main(): def main():
argument_spec = create_default_argspec(with_account=False) argument_spec = create_default_argspec(with_account=False)
argument_spec.update_argspec( argument_spec.update_argspec(
certificate_path=dict(type='path'), certificate_path=dict(type="path"),
certificate_content=dict(type='str'), certificate_content=dict(type="str"),
) )
argument_spec.update( argument_spec.update(
required_one_of=( required_one_of=(["certificate_path", "certificate_content"],),
['certificate_path', 'certificate_content'], mutually_exclusive=(["certificate_path", "certificate_content"],),
),
mutually_exclusive=(
['certificate_path', 'certificate_content'],
),
) )
module = argument_spec.create_ansible_module(supports_check_mode=True) module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True) backend = create_backend(module, True)
@@ -125,10 +124,12 @@ def main():
try: try:
client = ACMEClient(module, backend) client = ACMEClient(module, backend)
if not client.directory.has_renewal_info_endpoint(): if not client.directory.has_renewal_info_endpoint():
module.fail_json(msg='The ACME endpoint does not support ACME Renewal Information retrieval') module.fail_json(
msg="The ACME endpoint does not support ACME Renewal Information retrieval"
)
renewal_info = client.get_renewal_info( renewal_info = client.get_renewal_info(
cert_filename=module.params['certificate_path'], cert_filename=module.params["certificate_path"],
cert_content=module.params['certificate_content'], cert_content=module.params["certificate_content"],
include_retry_after=True, include_retry_after=True,
) )
module.exit_json(renewal_info=renewal_info) module.exit_json(renewal_info=renewal_info)
@@ -136,5 +137,5 @@ def main():
e.do_fail(module) e.do_fail(module)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -6,6 +6,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@@ -240,8 +242,8 @@ options:
type: str type: str
include_renewal_cert_id: include_renewal_cert_id:
description: description:
- Determines whether to request renewal of an existing certificate according to L(the ACME ARI draft 3, - Determines whether to request renewal of an existing certificate according to L(Section 5 of RFC 9773,
https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-5). https://www.rfc-editor.org/rfc/rfc9773.html#section-5).
- This is only used when the certificate specified in O(dest) or O(fullchain_dest) already exists. - This is only used when the certificate specified in O(dest) or O(fullchain_dest) already exists.
- Generally you should use V(when_ari_supported) if you know that the ACME service supports a compatible draft (or final - Generally you should use V(when_ari_supported) if you know that the ACME service supports a compatible draft (or final
version, once it is out) of the ARI extension. V(always) should never be necessary. If you are not sure, or if you version, once it is out) of the ARI extension. V(always) should never be necessary. If you are not sure, or if you
@@ -302,6 +304,7 @@ options:
""" """
EXAMPLES = r""" EXAMPLES = r"""
---
### Example with HTTP challenge ### ### Example with HTTP challenge ###
- name: Create a challenge for sample.com using a account key from a variable. - name: Create a challenge for sample.com using a account key from a variable.
@@ -356,6 +359,7 @@ EXAMPLES = r"""
chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt
data: "{{ sample_com_challenge }}" data: "{{ sample_com_challenge }}"
---
### Example with DNS challenge against production ACME server ### ### Example with DNS challenge against production ACME server ###
- name: Create a challenge for sample.com using a account key file. - name: Create a challenge for sample.com using a account key file.
@@ -565,118 +569,125 @@ all_chains:
import os import os
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
create_default_argspec,
ACMEClient,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.account import ( from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
ACMEAccount, ACMEAccount,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import ( ACMEClient,
normalize_combined_identifier, create_backend,
combine_identifier, create_default_argspec,
split_identifier,
wait_for_validation,
Authorization,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import ( from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import (
retrieve_acme_v1_certificate,
CertificateChain, CertificateChain,
Criterium, Criterium,
retrieve_acme_v1_certificate,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import (
Authorization,
combine_identifier,
normalize_combined_identifier,
split_identifier,
wait_for_validation,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ModuleFailException, ModuleFailException,
) )
from ansible_collections.community.crypto.plugins.module_utils.acme.io import write_file
from ansible_collections.community.crypto.plugins.module_utils.acme.io import ( from ansible_collections.community.crypto.plugins.module_utils.acme.orders import Order
write_file,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.orders import (
Order,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
compute_cert_id, compute_cert_id,
pem_to_der, pem_to_der,
) )
NO_CHALLENGE = 'no challenge' NO_CHALLENGE = "no challenge"
class ACMECertificateClient(object): class ACMECertificateClient(object):
''' """
ACME client class. Uses an ACME account object and a CSR to ACME client class. Uses an ACME account object and a CSR to
start and validate ACME challenges and download the respective start and validate ACME challenges and download the respective
certificates. certificates.
''' """
def __init__(self, module, backend): def __init__(self, module, backend):
self.module = module self.module = module
self.version = module.params['acme_version'] self.version = module.params["acme_version"]
self.challenge = module.params['challenge'] self.challenge = module.params["challenge"]
# We use None instead of a magic string for 'no challenge' # We use None instead of a magic string for 'no challenge'
if self.challenge == NO_CHALLENGE: if self.challenge == NO_CHALLENGE:
self.challenge = None self.challenge = None
self.csr = module.params['csr'] self.csr = module.params["csr"]
self.csr_content = module.params['csr_content'] self.csr_content = module.params["csr_content"]
self.dest = module.params.get('dest') self.dest = module.params.get("dest")
self.fullchain_dest = module.params.get('fullchain_dest') self.fullchain_dest = module.params.get("fullchain_dest")
self.chain_dest = module.params.get('chain_dest') self.chain_dest = module.params.get("chain_dest")
self.client = ACMEClient(module, backend) self.client = ACMEClient(module, backend)
self.account = ACMEAccount(self.client) self.account = ACMEAccount(self.client)
self.directory = self.client.directory self.directory = self.client.directory
self.data = module.params['data'] self.data = module.params["data"]
self.authorizations = None self.authorizations = None
self.cert_days = -1 self.cert_days = -1
self.order = None self.order = None
self.order_uri = self.data.get('order_uri') if self.data else None self.order_uri = self.data.get("order_uri") if self.data else None
self.all_chains = None self.all_chains = None
self.select_chain_matcher = [] self.select_chain_matcher = []
self.include_renewal_cert_id = module.params['include_renewal_cert_id'] self.include_renewal_cert_id = module.params["include_renewal_cert_id"]
self.profile = module.params['profile'] self.profile = module.params["profile"]
self.order_creation_error_strategy = module.params['order_creation_error_strategy'] self.order_creation_error_strategy = module.params[
self.order_creation_max_retries = module.params['order_creation_max_retries'] "order_creation_error_strategy"
]
self.order_creation_max_retries = module.params["order_creation_max_retries"]
if self.module.params['select_chain']: if self.module.params["select_chain"]:
for criterium_idx, criterium in enumerate(self.module.params['select_chain']): for criterium_idx, criterium in enumerate(
self.module.params["select_chain"]
):
try: try:
self.select_chain_matcher.append( self.select_chain_matcher.append(
self.client.backend.create_chain_matcher( self.client.backend.create_chain_matcher(
Criterium(criterium, index=criterium_idx))) Criterium(criterium, index=criterium_idx)
)
)
except ValueError as exc: except ValueError as exc:
self.module.warn('Error while parsing criterium: {error}. Ignoring criterium.'.format(error=exc)) self.module.warn(
"Error while parsing criterium: {error}. Ignoring criterium.".format(
error=exc
)
)
if self.profile is not None: if self.profile is not None:
meta_profiles = (self.directory.get('meta') or {}).get('profiles') or {} meta_profiles = (self.directory.get("meta") or {}).get("profiles") or {}
if not meta_profiles: if not meta_profiles:
raise ModuleFailException(msg='The ACME CA does not support profiles.') raise ModuleFailException(msg="The ACME CA does not support profiles.")
if self.profile not in meta_profiles: if self.profile not in meta_profiles:
raise ModuleFailException(msg='The ACME CA does not support selected profile {0!r}.'.format(self.profile)) raise ModuleFailException(
msg="The ACME CA does not support selected profile {0!r}.".format(
self.profile
)
)
# Make sure account exists # Make sure account exists
modify_account = module.params['modify_account'] modify_account = module.params["modify_account"]
if modify_account or self.version > 1: if modify_account or self.version > 1:
contact = [] contact = []
if module.params['account_email']: if module.params["account_email"]:
contact.append('mailto:' + module.params['account_email']) contact.append("mailto:" + module.params["account_email"])
created, account_data = self.account.setup_account( created, account_data = self.account.setup_account(
contact, contact,
agreement=module.params.get('agreement'), agreement=module.params.get("agreement"),
terms_agreed=module.params.get('terms_agreed'), terms_agreed=module.params.get("terms_agreed"),
allow_creation=modify_account, allow_creation=modify_account,
) )
if account_data is None: if account_data is None:
raise ModuleFailException(msg='Account does not exist or is deactivated.') raise ModuleFailException(
msg="Account does not exist or is deactivated."
)
updated = False updated = False
if not created and account_data and modify_account: if not created and account_data and modify_account:
updated, account_data = self.account.update_account(account_data, contact) updated, account_data = self.account.update_account(
account_data, contact
)
self.changed = created or updated self.changed = created or updated
else: else:
# This happens if modify_account is False and the ACME v1 # This happens if modify_account is False and the ACME v1
@@ -690,13 +701,15 @@ class ACMECertificateClient(object):
raise ModuleFailException("CSR %s not found" % (self.csr)) raise ModuleFailException("CSR %s not found" % (self.csr))
# Extract list of identifiers from CSR # Extract list of identifiers from CSR
self.identifiers = self.client.backend.get_ordered_csr_identifiers(csr_filename=self.csr, csr_content=self.csr_content) self.identifiers = self.client.backend.get_ordered_csr_identifiers(
csr_filename=self.csr, csr_content=self.csr_content
)
def is_first_step(self): def is_first_step(self):
''' """
Return True if this is the first execution of this module, i.e. if a Return True if this is the first execution of this module, i.e. if a
sufficient data object from a first run has not been provided. sufficient data object from a first run has not been provided.
''' """
if self.data is None: if self.data is None:
return True return True
if self.version == 1: if self.version == 1:
@@ -708,32 +721,34 @@ class ACMECertificateClient(object):
return self.order_uri is None return self.order_uri is None
def _get_cert_info_or_none(self): def _get_cert_info_or_none(self):
if self.module.params.get('dest'): if self.module.params.get("dest"):
filename = self.module.params['dest'] filename = self.module.params["dest"]
else: else:
filename = self.module.params['fullchain_dest'] filename = self.module.params["fullchain_dest"]
if not os.path.exists(filename): if not os.path.exists(filename):
return None return None
return self.client.backend.get_cert_information(cert_filename=filename) return self.client.backend.get_cert_information(cert_filename=filename)
def start_challenges(self): def start_challenges(self):
''' """
Create new authorizations for all identifiers of the CSR, Create new authorizations for all identifiers of the CSR,
respectively start a new order for ACME v2. respectively start a new order for ACME v2.
''' """
self.authorizations = {} self.authorizations = {}
if self.version == 1: if self.version == 1:
for identifier_type, identifier in self.identifiers: for identifier_type, identifier in self.identifiers:
if identifier_type != 'dns': if identifier_type != "dns":
raise ModuleFailException('ACME v1 only supports DNS identifiers!') raise ModuleFailException("ACME v1 only supports DNS identifiers!")
for identifier_type, identifier in self.identifiers: for identifier_type, identifier in self.identifiers:
authz = Authorization.create(self.client, identifier_type, identifier) authz = Authorization.create(self.client, identifier_type, identifier)
self.authorizations[normalize_combined_identifier(authz.combined_identifier)] = authz self.authorizations[
normalize_combined_identifier(authz.combined_identifier)
] = authz
else: else:
replaces_cert_id = None replaces_cert_id = None
if ( if self.include_renewal_cert_id == "always" or (
self.include_renewal_cert_id == 'always' or self.include_renewal_cert_id == "when_ari_supported"
(self.include_renewal_cert_id == 'when_ari_supported' and self.client.directory.has_renewal_info_endpoint()) and self.client.directory.has_renewal_info_endpoint()
): ):
cert_info = self._get_cert_info_or_none() cert_info = self._get_cert_info_or_none()
if cert_info is not None: if cert_info is not None:
@@ -757,39 +772,46 @@ class ACMECertificateClient(object):
self.changed = True self.changed = True
def get_challenges_data(self, first_step): def get_challenges_data(self, first_step):
''' """
Get challenge details for the chosen challenge type. Get challenge details for the chosen challenge type.
Return a tuple of generic challenge details, and specialized DNS challenge details. Return a tuple of generic challenge details, and specialized DNS challenge details.
''' """
# Get general challenge data # Get general challenge data
data = {} data = {}
for type_identifier, authz in self.authorizations.items(): for type_identifier, authz in self.authorizations.items():
identifier_type, identifier = split_identifier(type_identifier) identifier_type, identifier = split_identifier(type_identifier)
# Skip valid authentications: their challenges are already valid # Skip valid authentications: their challenges are already valid
# and do not need to be returned # and do not need to be returned
if authz.status == 'valid': if authz.status == "valid":
continue continue
# We drop the type from the key to preserve backwards compatibility # We drop the type from the key to preserve backwards compatibility
data[authz.identifier] = authz.get_challenge_data(self.client) data[authz.identifier] = authz.get_challenge_data(self.client)
if first_step and self.challenge is not None and self.challenge not in data[authz.identifier]: if (
raise ModuleFailException("Found no challenge of type '{0}' for identifier {1}!".format( first_step
self.challenge, type_identifier)) and self.challenge is not None
and self.challenge not in data[authz.identifier]
):
raise ModuleFailException(
"Found no challenge of type '{0}' for identifier {1}!".format(
self.challenge, type_identifier
)
)
# Get DNS challenge data # Get DNS challenge data
data_dns = {} data_dns = {}
if self.challenge == 'dns-01': if self.challenge == "dns-01":
for identifier, challenges in data.items(): for identifier, challenges in data.items():
if self.challenge in challenges: if self.challenge in challenges:
values = data_dns.get(challenges[self.challenge]['record']) values = data_dns.get(challenges[self.challenge]["record"])
if values is None: if values is None:
values = [] values = []
data_dns[challenges[self.challenge]['record']] = values data_dns[challenges[self.challenge]["record"]] = values
values.append(challenges[self.challenge]['resource_value']) values.append(challenges[self.challenge]["resource_value"])
return data, data_dns return data, data_dns
def finish_challenges(self): def finish_challenges(self):
''' """
Verify challenges for all identifiers of the CSR. Verify challenges for all identifiers of the CSR.
''' """
self.authorizations = {} self.authorizations = {}
# Step 1: obtain challenge information # Step 1: obtain challenge information
@@ -798,7 +820,9 @@ class ACMECertificateClient(object):
# will be returned instead. # will be returned instead.
for identifier_type, identifier in self.identifiers: for identifier_type, identifier in self.identifiers:
authz = Authorization.create(self.client, identifier_type, identifier) authz = Authorization.create(self.client, identifier_type, identifier)
self.authorizations[combine_identifier(identifier_type, identifier)] = authz self.authorizations[combine_identifier(identifier_type, identifier)] = (
authz
)
else: else:
# For ACME v2, we obtain the order object by fetching the # For ACME v2, we obtain the order object by fetching the
# order URI, and extract the information from there. # order URI, and extract the information from there.
@@ -809,12 +833,12 @@ class ACMECertificateClient(object):
# Step 2: validate pending challenges # Step 2: validate pending challenges
authzs_to_wait_for = [] authzs_to_wait_for = []
for type_identifier, authz in self.authorizations.items(): for type_identifier, authz in self.authorizations.items():
if authz.status == 'pending': if authz.status == "pending":
if self.challenge is not None: if self.challenge is not None:
authz.call_validate(self.client, self.challenge, wait=False) authz.call_validate(self.client, self.challenge, wait=False)
authzs_to_wait_for.append(authz) authzs_to_wait_for.append(authz)
# If there is no challenge, we must check whether the authz is valid # If there is no challenge, we must check whether the authz is valid
elif authz.status != 'valid': elif authz.status != "valid":
authz.raise_error( authz.raise_error(
'Status is not "valid", even though no challenge should be necessary', 'Status is not "valid", even though no challenge should be necessary',
module=self.client.module, module=self.client.module,
@@ -830,7 +854,11 @@ class ACMECertificateClient(object):
try: try:
alt_cert = CertificateChain.download(self.client, alternate) alt_cert = CertificateChain.download(self.client, alternate)
except ModuleFailException as e: except ModuleFailException as e:
self.module.warn('Error while downloading alternative certificate {0}: {1}'.format(alternate, e)) self.module.warn(
"Error while downloading alternative certificate {0}: {1}".format(
alternate, e
)
)
continue continue
alternate_chains.append(alt_cert) alternate_chains.append(alt_cert)
return alternate_chains return alternate_chains
@@ -839,35 +867,52 @@ class ACMECertificateClient(object):
for criterium_idx, matcher in enumerate(self.select_chain_matcher): for criterium_idx, matcher in enumerate(self.select_chain_matcher):
for chain in chains: for chain in chains:
if matcher.match(chain): if matcher.match(chain):
self.module.debug('Found matching chain for criterium {0}'.format(criterium_idx)) self.module.debug(
"Found matching chain for criterium {0}".format(criterium_idx)
)
return chain return chain
return None return None
def get_certificate(self): def get_certificate(self):
''' """
Request a new certificate and write it to the destination file. Request a new certificate and write it to the destination file.
First verifies whether all authorizations are valid; if not, aborts First verifies whether all authorizations are valid; if not, aborts
with an error. with an error.
''' """
for identifier_type, identifier in self.identifiers: for identifier_type, identifier in self.identifiers:
authz = self.authorizations.get(normalize_combined_identifier(combine_identifier(identifier_type, identifier))) authz = self.authorizations.get(
normalize_combined_identifier(
combine_identifier(identifier_type, identifier)
)
)
if authz is None: if authz is None:
raise ModuleFailException('Found no authorization information for "{identifier}"!'.format( raise ModuleFailException(
identifier=combine_identifier(identifier_type, identifier))) 'Found no authorization information for "{identifier}"!'.format(
if authz.status != 'valid': identifier=combine_identifier(identifier_type, identifier)
authz.raise_error('Status is "{status}" and not "valid"'.format(status=authz.status), module=self.module) )
)
if authz.status != "valid":
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)
)
else: else:
self.order.finalize(self.client, pem_to_der(self.csr, self.csr_content)) self.order.finalize(self.client, pem_to_der(self.csr, self.csr_content))
cert = CertificateChain.download(self.client, self.order.certificate_uri) cert = CertificateChain.download(self.client, self.order.certificate_uri)
if self.module.params['retrieve_all_alternates'] or self.select_chain_matcher: if (
self.module.params["retrieve_all_alternates"]
or self.select_chain_matcher
):
# Retrieve alternate chains # Retrieve alternate chains
alternate_chains = self.download_alternate_chains(cert) alternate_chains = self.download_alternate_chains(cert)
# Prepare return value for all alternate chains # Prepare return value for all alternate chains
if self.module.params['retrieve_all_alternates']: if self.module.params["retrieve_all_alternates"]:
self.all_chains = [cert.to_json()] self.all_chains = [cert.to_json()]
for alt_chain in alternate_chains: for alt_chain in alternate_chains:
self.all_chains.append(alt_chain.to_json()) self.all_chains.append(alt_chain.to_json())
@@ -878,89 +923,122 @@ class ACMECertificateClient(object):
if matching_chain: if matching_chain:
cert = matching_chain cert = matching_chain
else: else:
self.module.debug('Found no matching alternative chain') self.module.debug("Found no matching alternative chain")
if cert.cert is not None: if cert.cert is not None:
pem_cert = cert.cert pem_cert = cert.cert
chain = cert.chain chain = cert.chain
if self.dest and write_file(self.module, self.dest, pem_cert.encode('utf8')): if self.dest and write_file(
self.module, self.dest, pem_cert.encode("utf8")
):
self.cert_days = self.client.backend.get_cert_days(self.dest) self.cert_days = self.client.backend.get_cert_days(self.dest)
self.changed = True self.changed = True
if self.fullchain_dest and write_file(self.module, self.fullchain_dest, (pem_cert + "\n".join(chain)).encode('utf8')): if self.fullchain_dest and write_file(
self.module,
self.fullchain_dest,
(pem_cert + "\n".join(chain)).encode("utf8"),
):
self.cert_days = self.client.backend.get_cert_days(self.fullchain_dest) self.cert_days = self.client.backend.get_cert_days(self.fullchain_dest)
self.changed = True self.changed = True
if self.chain_dest and write_file(self.module, self.chain_dest, ("\n".join(chain)).encode('utf8')): if self.chain_dest and write_file(
self.module, self.chain_dest, ("\n".join(chain)).encode("utf8")
):
self.changed = True self.changed = True
def deactivate_authzs(self): def deactivate_authzs(self):
''' """
Deactivates all valid authz's. Does not raise exceptions. Deactivates all valid authz's. Does not raise exceptions.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2 https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2 https://tools.ietf.org/html/rfc8555#section-7.5.2
''' """
for authz in self.authorizations.values(): for authz in self.authorizations.values():
try: try:
authz.deactivate(self.client) authz.deactivate(self.client)
except Exception: except Exception:
# ignore errors # ignore errors
pass pass
if authz.status != 'deactivated': if authz.status != "deactivated":
self.module.warn(warning='Could not deactivate authz object {0}.'.format(authz.url)) self.module.warn(
warning="Could not deactivate authz object {0}.".format(authz.url)
)
def main(): def main():
argument_spec = create_default_argspec(with_certificate=True) argument_spec = create_default_argspec(with_certificate=True)
argument_spec.argument_spec['csr']['aliases'] = ['src'] argument_spec.argument_spec["csr"]["aliases"] = ["src"]
argument_spec.update_argspec( argument_spec.update_argspec(
modify_account=dict(type='bool', default=True), modify_account=dict(type="bool", default=True),
account_email=dict(type='str'), account_email=dict(type="str"),
agreement=dict(type='str'), agreement=dict(type="str"),
terms_agreed=dict(type='bool', default=False), terms_agreed=dict(type="bool", default=False),
challenge=dict(type='str', default='http-01', choices=['http-01', 'dns-01', 'tls-alpn-01', NO_CHALLENGE]), challenge=dict(
data=dict(type='dict'), type="str",
dest=dict(type='path', aliases=['cert']), default="http-01",
fullchain_dest=dict(type='path', aliases=['fullchain']), choices=["http-01", "dns-01", "tls-alpn-01", NO_CHALLENGE],
chain_dest=dict(type='path', aliases=['chain']), ),
remaining_days=dict(type='int', default=10), data=dict(type="dict"),
deactivate_authzs=dict(type='bool', default=False), dest=dict(type="path", aliases=["cert"]),
force=dict(type='bool', default=False), fullchain_dest=dict(type="path", aliases=["fullchain"]),
retrieve_all_alternates=dict(type='bool', default=False), chain_dest=dict(type="path", aliases=["chain"]),
select_chain=dict(type='list', elements='dict', options=dict( remaining_days=dict(type="int", default=10),
test_certificates=dict(type='str', default='all', choices=['first', 'last', 'all']), deactivate_authzs=dict(type="bool", default=False),
issuer=dict(type='dict'), force=dict(type="bool", default=False),
subject=dict(type='dict'), retrieve_all_alternates=dict(type="bool", default=False),
subject_key_identifier=dict(type='str'), select_chain=dict(
authority_key_identifier=dict(type='str'), type="list",
)), elements="dict",
include_renewal_cert_id=dict(type='str', choices=['never', 'when_ari_supported', 'always'], default='never'), options=dict(
profile=dict(type='str'), test_certificates=dict(
order_creation_error_strategy=dict(type='str', default='auto', choices=['auto', 'always', 'fail', 'retry_without_replaces_cert_id']), type="str", default="all", choices=["first", "last", "all"]
order_creation_max_retries=dict(type='int', default=3), ),
issuer=dict(type="dict"),
subject=dict(type="dict"),
subject_key_identifier=dict(type="str"),
authority_key_identifier=dict(type="str"),
),
),
include_renewal_cert_id=dict(
type="str",
choices=["never", "when_ari_supported", "always"],
default="never",
),
profile=dict(type="str"),
order_creation_error_strategy=dict(
type="str",
default="auto",
choices=["auto", "always", "fail", "retry_without_replaces_cert_id"],
),
order_creation_max_retries=dict(type="int", default=3),
) )
argument_spec.update( argument_spec.update(
required_one_of=[ required_one_of=[
['dest', 'fullchain_dest'], ["dest", "fullchain_dest"],
], ],
) )
module = argument_spec.create_ansible_module(supports_check_mode=True) module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, False) backend = create_backend(module, False)
try: try:
if module.params.get('dest'): if module.params.get("dest"):
cert_days = backend.get_cert_days(module.params['dest']) cert_days = backend.get_cert_days(module.params["dest"])
else: else:
cert_days = backend.get_cert_days(module.params['fullchain_dest']) cert_days = backend.get_cert_days(module.params["fullchain_dest"])
if module.params['force'] or cert_days < module.params['remaining_days']: if module.params["force"] or cert_days < module.params["remaining_days"]:
# If checkmode is active, base the changed state solely on the status # If checkmode is active, base the changed state solely on the status
# of the certificate file as all other actions (accessing an account, checking # of the certificate file as all other actions (accessing an account, checking
# the authorization status...) would lead to potential changes of the current # the authorization status...) would lead to potential changes of the current
# state # state
if module.check_mode: if module.check_mode:
module.exit_json(changed=True, authorizations={}, challenge_data={}, cert_days=cert_days) module.exit_json(
changed=True,
authorizations={},
challenge_data={},
cert_days=cert_days,
)
else: else:
client = ACMECertificateClient(module, backend) client = ACMECertificateClient(module, backend)
client.cert_days = cert_days client.cert_days = cert_days
@@ -975,9 +1053,9 @@ def main():
client.finish_challenges() client.finish_challenges()
client.get_certificate() client.get_certificate()
if client.all_chains is not None: if client.all_chains is not None:
other['all_chains'] = client.all_chains other["all_chains"] = client.all_chains
finally: finally:
if module.params['deactivate_authzs']: if module.params["deactivate_authzs"]:
client.deactivate_authzs() client.deactivate_authzs()
data, data_dns = client.get_challenges_data(first_step=is_first_step) data, data_dns = client.get_challenges_data(first_step=is_first_step)
auths = dict() auths = dict()
@@ -1001,5 +1079,5 @@ def main():
e.do_fail(module) e.do_fail(module)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

Some files were not shown because too many files have changed in this diff Show More