Compare commits

...

327 Commits

Author SHA1 Message Date
Felix Fontein
94416989a8 Release 3.0.0-a1. 2025-05-18 14:33:13 +02:00
Felix Fontein
b08afe4237 Make all doc_fragments private. (#898) 2025-05-18 01:42:18 +02:00
Felix Fontein
7294841a28 Replace to_native with to_text. (#897) 2025-05-18 01:31:33 +02:00
Felix Fontein
9b8e4e81a9 Forgot to mention cryptography. 2025-05-18 01:31:21 +02:00
Felix Fontein
efda8596a5 Prepare 3.0.0-a1 release. 2025-05-18 01:09:30 +02:00
Felix Fontein
318462fa24 Work on issues found by pylint (#896)
* Look at possibly-used-before-assignment.

* Use latest beta releases of ansible-core 2.19 for mypy and pylint.

* Look at unsupported-*.

* Look at unknown-option-value.

* Look at redefined-builtin.

* Look at superfluous-parens.

* Look at unspecified-encoding.

* Adjust to new cryptography version and to ansible-core 2.17's pylint.

* Look at super-with-arguments.

* Look at no-else-*.

* Look at try-except-raise.

* Look at inconsistent-return-statements.

* Look at redefined-outer-name.

* Look at redefined-argument-from-local.

* Look at attribute-defined-outside-init.

* Look at unused-variable.

* Look at protected-access.

* Look at raise-missing-from.

* Look at arguments-differ.

* Look at useless-suppression and use-symbolic-message-instead.

* Look at consider-using-dict-items.

* Look at consider-using-in.

* Look at consider-using-set-comprehension.

* Look at consider-using-with.

* Look at use-dict-literal.
2025-05-18 00:57:28 +02:00
Felix Fontein
a3a5284f97 Add basic typing for Entrust code. (#894) 2025-05-17 17:43:50 +02:00
Felix Fontein
990b40df3e Add pylint (#892)
* Move mypy/flake8/isort config files to more 'natural' places.

* Add pylint.

* Look at no-member.

* Look at pointless-* and unnecessary-pass.

* Look at useless-*.

* Lint.
2025-05-17 16:45:37 +02:00
Felix Fontein
5fbf35df86 Deprecate no longer used options. (#891) 2025-05-16 22:23:05 +02:00
Felix Fontein
56f004dc63 More refactorings (#890)
* Improve typing.

* Improve version parameter validation for x509_certificate* modules.

* Use utils for parsing retry-after.
2025-05-16 21:53:18 +02:00
Felix Fontein
44bcc8cebc Code refactoring (#889)
* Add __all__ to all module and plugin utils.

* Convert quite a few positional args to keyword args.

* Avoid Python 3.8+ syntax.
2025-05-16 06:55:57 +02:00
Felix Fontein
a5a4e022ba Make all module_utils and plugin_utils private (#887)
* Add leading underscore. Remove deprecated module utils.

* Document module and plugin utils as private. Add changelog fragment.

* Convert relative to absolute imports.

* Remove unnecessary imports.
2025-05-11 19:17:58 +02:00
Felix Fontein
f758d94fba Add type hints and type checking (#885)
* Enable basic type checking.

* Fix first errors.

* Add changelog fragment.

* Add types to module_utils and plugin_utils (without module backends).

* Add typing hints for acme_* modules.

* Add typing to X.509 certificate modules, and add more helpers.

* Add typing to remaining module backends.

* Add typing for action, filter, and lookup plugins.

* Bump ansible-core 2.19 beta requirement for typing.

* Add more typing definitions.

* Add typing to some unit tests.
2025-05-11 18:00:11 +02:00
Felix Fontein
82f0176773 Remove ignore.txt files for ansible-core < 2.17. 2025-05-04 21:42:29 +02:00
Felix Fontein
8156468898 Add ansible-lint to CI (#886)
* Enable ansible-lint.

* Fix broken task name.

* Fix command-instead-of-shell instances.

* Clean up tasks to eliminate command-instead-of-module.

* Skip yaml errors.

* Remove .stdout from versions.

* Avoid stdin.
2025-05-03 14:42:41 +02:00
Felix Fontein
12f958c955 Fix assert_required_cryptography_version() calls. 2025-05-03 12:55:50 +02:00
Felix Fontein
83beb7148c Remove six usages. (#884) 2025-05-03 11:12:29 +02:00
Felix Fontein
645b7bf9ed Get rid of backend parameter whenever possible (#883)
* Get rid of backend parameter whenever possible.

* Always auto-detect if backend choices are 'cryptography' and 'auto', resp. always check cryptography version.

* Improve error message.

* Update documentation.
2025-05-03 10:46:53 +02:00
Felix Fontein
fbcb89f092 Support cryptography 3.3 (#882)
* Re-add Debian Bullseye to CI.

* Support cryptography 3.3 as well.
2025-05-02 21:42:06 +02:00
Felix Fontein
86db561193 Get rid of some to_native and to_text calls. (#880) 2025-05-02 15:58:39 +02:00
Felix Fontein
0b8f3306c7 Use unittest.mock. (#881) 2025-05-02 15:39:03 +02:00
Felix Fontein
5231ac8f3f Remove support for cryptography < 3.4 (#878)
* Stop passing backend to cryptography.

* Make public_bytes() fallback the default.

* Remove compatibility code for older cryptography versions.

* Require cryptography 3.4+.

* Restrict to cryptography >= 3.4 in integration tests.

* Remove Debian Bullseye from CI.

It only supports cryptography 3.3.

* Improve imports.

* Remove no longer existing conditional.
2025-05-02 15:27:18 +02:00
Felix Fontein
e8fec768cc Remove prepare_jinja2_compat. (#879) 2025-05-02 13:18:19 +02:00
Felix Fontein
ef230011fd Lint doc fragments. 2025-05-01 16:47:59 +02:00
Felix Fontein
65872e884f Remove Python 2 specific code (#877)
* Get rid of Python 2 special handling.

* Get rid of more Python 2 specific handling.

* Stop using six.

* ipaddress is part of the standard library since Python 3.

* Add changelog.

* Fix import.

* Remove unneeded imports.
2025-05-01 16:21:13 +02:00
Felix Fontein
641e63b08c Replace % and str.format() with f-strings (#875)
* Replace % and str.format() with f-strings.

* Apply suggestions from review.
2025-05-01 11:50:10 +02:00
Felix Fontein
d8f838c365 Modernize some Python constructs (#876)
* Update __future__ import, remove __metaclass__ assignment.

* Removing obsolete encoding comment.

* Remove unneccessary object inheritance.
2025-05-01 10:36:59 +02:00
Felix Fontein
266082db72 Remove more traces of PyOpenSSL, including from EE dependencies (#874)
* Remove PyOpenSSL backends.

* Remove EOL ansible-core's from EE builds.

* Update Pythons in EEs.

* Remove pyopenssl tests.
2025-04-29 09:33:21 +02:00
Felix Fontein
718021b714 Fix typo. 2025-04-29 08:13:41 +02:00
Felix Fontein
d368d1943d Bump version to 3.0.0-dev0, remove deprecated functionality and implement announced breaking changes (#873)
* Bump verison to 3.0.0-dev0.

* Change check mode behavior for *_pipe modules.

* Remove PyOpenSSL backend.

* Remove PyOpenSSL setup.

* Change default of asn1_base64.

* Remove deprecated common module utils.

* Remove get_default_argspec().

* Mark two methods as abstract.

* Remove ACME v1 support.

* Remove retrieve_acme_v1_certificate().

* Remove deprecated docs fragment.

* Change meaning of mode parameter.

* Mark no longer used option as 'to deprecate'.
2025-04-29 08:12:44 +02:00
Felix Fontein
f73a1ce590 Drop compatibility with older versions. (#872) 2025-04-28 21:45:42 +02:00
Felix Fontein
5bcbd4d0f4 Enable formatting with black; add reformat commit to .git-blame-ignore-revs. 2025-04-28 20:35:32 +02:00
Felix Fontein
797bd8a6e2 Reformat again with black, this time without Python 2 workarounds. 2025-04-28 20:34:38 +02:00
Felix Fontein
23de865563 Unvendor distutils.version (#371)
* Unvendor distutils.version.

* Fix version.

* Assume the collection requires ansible-core 2.12+.

This is valid since this only get merged for 3.0.0, which
will drop support for quite a few more ansible-core versions.

* Mark for re-export.
2025-04-28 14:30:37 +02:00
Felix Fontein
4e8a0e456b Prepare basic 3.0.0 setup (#870)
* Drop support for ansible-core < 2.17.

* Galaxy can show included content nowadays. (Not perfect, but a lot better than before.)

* This should have been removed a long time ago.
2025-04-28 12:39:28 +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
Felix Fontein
3d4c5346c6 Release 2.24.0. 2025-01-19 13:03:31 +01:00
Felix Fontein
a8aa05ac4e Avoid reserved variable name 'order'. 2025-01-19 10:59:55 +01:00
Felix Fontein
0e122e5f56 Improve ACME profile support. 2025-01-19 10:55:26 +01:00
Felix Fontein
47ea1af180 Forgot to adjust warnings. 2025-01-19 10:47:24 +01:00
Felix Fontein
3951e6ceb4 Include cert ID in warning.
This prevents the warning to be not shown for different certificates
in the same playbook due to warning de-duplication.
2025-01-19 08:58:49 +01:00
Felix Fontein
bf70f8d717 Prepare 2.24.0. 2025-01-18 11:25:37 +01:00
Felix Fontein
214794d056 acme_certificate and acme_certificate_create_order: add order_creation_error_strategy and order_creation_max_retries options (#842)
* Provide error information.

* Add helper function for order creation retrying.

* Improve existing documentation.

* Document 'replaces' return value.

* Add order_creation_error_strategy and order_creation_max_retries options.

* Add changelog fragment.

* Fix authz deactivation for finalizing step.

* Fix profile handling on order creation.

* Improve existing tests.

* Add ARI and profile tests.

* Warn when 'replaces' is removed when retrying to create an order.
2025-01-18 10:51:10 +01:00
Felix Fontein
b9fa5b5193 Deprecate ansible-core < 2.17 and cryptography < 3.4. (#839) 2025-01-17 21:27:01 +00:00
Felix Fontein
5366b9e5ba Improve ACME tests; add acme_ari_info tests; use ARI and profiles features in acme_certificate tests (#841)
* Fix description.

* Add basic acme_ari_info test.

* Refactoring.

* Extend acme_certificate tests.
2025-01-14 23:49:24 +01:00
Felix Fontein
fd67767538 Move EOL'ed ansible-core 2.15 from AZP to GHA (#840)
* Move EOL'ed ansible-core 2.15 from AZP to GHA.

* CentOS 7 does not work in GHA.
2025-01-14 19:31:03 +01:00
Felix Fontein
ae35be3437 Adjust ARI tests to new Pebble (#837)
* Adjust ARI tests to new Pebble.

* Fix key size for certificates to 2048 on all systems.
2025-01-13 21:43:29 +01:00
Felix Fontein
01e7bf1f33 acme_certificate_renewal_info: add treat_parsing_error_as_non_existing option and existing and parsable return values (#838)
* Fix error reporting for OpenSSL backend: raise BackendExceptions instead of directly failing the module.

* Add treat_parsing_error_as_non_existing option and existing and parsable return values.
2025-01-12 21:42:24 +01:00
Felix Fontein
49354f2121 Add new ACME modules for working with orders. (#757) 2025-01-12 17:10:58 +01:00
Felix Fontein
072318466e Update ACME tests (#836)
* Restrict remaining days to also work with short-lived profiles.

* Adjust boolean cases.

* Fix spelling error.

* Use larger key size for TLS-ALPN test certificate.
2025-01-12 13:59:08 +01:00
Felix Fontein
248250514f Fix profile implementation. 2025-01-12 13:57:17 +01:00
Felix Fontein
2419e6c6ad Implement profile option. (#835) 2025-01-12 10:24:24 +01:00
Felix Fontein
029e009db1 CI: Add Fedora 41, Alpine 3.21, RHEL 9.5, FreeBSD 14.2 to CI for devel (#834)
* Add Fedora 41, Alpine 3.21, RHEL 9.5, FreeBSD 14.2 to CI for devel.

* Fedora 41 also doesn't allow SHA-1 apparently.

Ref: https://fedoraproject.org/wiki/Changes/OpenSSLDistrustSHA1SigVer

* Work around broken cryptography in Fedora 41.
2025-01-08 22:08:18 +01:00
Felix Fontein
cfd524f345 Fix CI badge image URL. Add documentation badge. 2025-01-04 11:27:22 +01:00
Felix Fontein
355480601d Make 2.9, 2.10, and 2.11 sanity tests shut up. 2025-01-03 15:26:19 +01:00
Felix Fontein
f956ddcc77 Add extra sanity test for acme action group. 2025-01-03 14:56:36 +01:00
Felix Fontein
ccb4ecfbd8 The next expected release will be 2.24.0. 2024-12-30 22:36:49 +01:00
Felix Fontein
95886d1cf9 Release 2.23.0. 2024-12-30 22:04:25 +01:00
Felix Fontein
9b53f4b382 Prepare 2.23.0 release. 2024-12-30 21:17:40 +01:00
Felix Fontein
3f0e292246 Add 'idempotent' attribute (#833)
* Add 'idempotent' attribute.

* Mention check mode in attribute description.

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>

---------

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2024-12-30 21:11:12 +01:00
Felix Fontein
0d4b16aadb acme_certificate: be nicer to non-compliant CAs (#832)
* Be nicer to non-compliant CAs.

* Mark as a feature, not a bugfix.
2024-12-30 10:30:33 +00:00
Felix Fontein
db04914ab6 Deprecate PyOpenSSL. (#831) 2024-12-30 10:10:10 +01:00
Felix Fontein
abb0d67774 Add validation option. (#830) 2024-12-30 10:09:51 +01:00
Felix Fontein
05c442ab5e luks_device: allow to provide passphrases base64-encoded (#829)
* Allow to provide passphrases base64-encoded.

* Add note on binary passphrases.
2024-12-30 10:09:32 +01:00
Felix Fontein
4ce9745d35 Put appropriate module attributes into doc fragments. 2024-12-29 16:17:03 +01:00
Felix Fontein
37af200ecb Fix doc fragments indents. 2024-12-29 15:47:51 +01:00
Felix Fontein
ddbcf49868 Improve formulations. 2024-12-28 17:02:42 +01:00
Felix Fontein
942be86635 Reformat documentation with 'andebox yaml-doc' (#828)
* Reformat documentation with 'andebox yaml-doc'.

* Fix/improve.

* Remaining fixes.

* One more.
2024-12-28 16:00:28 +00:00
Felix Fontein
2ed7f69b83 Improve language. 2024-12-28 14:30:08 +01:00
Felix Fontein
91504cda85 Arch Linux updated to Python 3.13. (#826) 2024-12-22 21:27:54 +01:00
Felix Fontein
16434d9ad8 Fix some issues pointed out by zizmor. (#823) 2024-12-14 14:56:00 +01:00
Felix Fontein
9e10cfb53a Update the PKCS#12 encryption warning. (#820) 2024-11-23 15:44:06 +01:00
Felix Fontein
32047dccc5 Add test with device name starting with 'crypt'. (#821) 2024-11-21 21:46:26 +01:00
Felix Fontein
0f7c5f0de1 CI: Fix cryptsetup version for RHEL 9.1/9.2/9.3/9.4 (#819)
* Fix cryptsetup version for RHEL 9.1/9.2/9.3.

* Also fix version for RHEL 9.4.

* Trigger change in openssh_cert.

* Use lower-case names.

* Actually install the right version.
2024-11-18 21:36:53 +01:00
dependabot[bot]
8b831dbe59 Bump fsfe/reuse-action from 4 to 5 (#818)
Bumps [fsfe/reuse-action](https://github.com/fsfe/reuse-action) from 4 to 5.
- [Release notes](https://github.com/fsfe/reuse-action/releases)
- [Commits](https://github.com/fsfe/reuse-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: fsfe/reuse-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 19:38:22 +01:00
Felix Fontein
8e33aafdba Add FreeBSD 13.4 to CI. (#815) 2024-11-08 23:05:44 +01:00
Felix Fontein
1b134f2d13 Next expected release will be 2.23.0. 2024-10-27 09:19:31 +01:00
Felix Fontein
7adca3efff Release 2.22.3. 2024-10-27 08:49:30 +01:00
Felix Fontein
6731b38baa Explicitly use UTC timezone in ACME OpenSSL backend (#811)
* Allow abstract backend class to handle both with and without timezone.

* Explicitly use UTC timezone in OpenSSL backend code.
2024-10-27 08:13:05 +01:00
Felix Fontein
feee571bc8 Fix time code to work in timezones other than UTC, and add tests in multiple timezones (#810)
* Add tests in multiple timezones.

* Fix get_epoch_seconds() for timestamps without timezones.

* Add changelog fragment.

* Pin version for Python 2.6.
2024-10-24 20:24:55 +02:00
Felix Fontein
21e344e283 Prepare 2.22.3 release. 2024-10-23 21:24:21 +02:00
Felix Fontein
7c93b61532 Fix reuse workflow branches. 2024-10-19 12:34:56 +02:00
Felix Fontein
dd8b90f9d3 Next expected release is 2.23.0. 2024-10-15 20:52:43 +02:00
Felix Fontein
e1c0ab5bd2 Release 2.22.2. 2024-10-15 20:34:01 +02:00
Felix Fontein
a57be5ceb3 Prepare 2.22.2 release. 2024-10-15 20:11:26 +02:00
Felix Fontein
6d4a8435c7 Add test for mixed-case DNS name. (#807) 2024-10-15 20:10:00 +02:00
Lyas Spiehler
a39b3bc882 lookup lowercase domain names when verifying authorizations to preven… (#803)
* lookup lowercase domain names when verifying authorizations to prevent failure when CSR has mixed-case names

Signed-off-by: Lyas Spiehler <lspiehler@gmail.com>

* remove .lower() method

* make authorizations keys lowercase

Signed-off-by: Lyas Spiehler <lspiehler@gmail.com>

* use lowercase keys for authorizations dict

Signed-off-by: Lyas Spiehler <lspiehler@gmail.com>

* use new normalize_combined_identifier function to normalize identifiers

* include two blank lines after functions to pass tests

* Update plugins/module_utils/acme/challenges.py

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

* add changelog fragment

Signed-off-by: Lyas Spiehler <lspiehler@gmail.com>

* Update changelogs/fragments/803-fix-authorization-failure-with-mixed-case-sans.yml

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

---------

Signed-off-by: Lyas Spiehler <lspiehler@gmail.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
2024-10-15 19:48:47 +02:00
Felix Fontein
30a16c8f60 Update SOPS example. (#806) 2024-10-03 22:33:47 +02:00
Felix Fontein
0638512cf9 Next expected release is 2.23.0. 2024-10-01 23:15:32 +03:00
Felix Fontein
4ee90e5ea2 Release 2.22.1. 2024-10-01 22:51:02 +03:00
Felix Fontein
9cdd28c2ca Prepare 2.22.1. 2024-10-01 22:34:15 +03:00
Felix Fontein
db871c2686 Pass absolute paths to atmoic_move(). (#799) 2024-10-01 21:55:00 +03:00
Felix Fontein
5a2dff7b74 Make sure that the required slash is present. (#802) 2024-09-29 21:26:31 +03:00
Felix Fontein
2d82f49adc Make sure idna is installed. (#800) 2024-09-25 23:10:44 +03:00
Felix Fontein
1095c0be41 Add stable-2.18 to CI (#798)
* Add stable-2.18 to CI.

* get_certificate: always use asn1_base64=true for ansible-core 2.18+.
2024-09-24 14:19:42 +03:00
Felix Fontein
019b0fd725 Next expected release is 2.23.0. 2024-09-08 17:00:45 +02:00
Felix Fontein
80c129941a Release 2.22.0. 2024-09-08 16:43:33 +02:00
Felix Fontein
eeb6152703 Remove link to Google Groups mailing list. (#795)
Ref: https://groups.google.com/g/ansible-project/c/B0oKR0aQqXs
2024-09-08 16:15:52 +02:00
Felix Fontein
e140642ba4 Prepare 2.22.0. 2024-09-07 09:53:13 +02:00
Felix Fontein
a49711d383 openssl_privatekey*: add default value for cipher option (#794)
* Add default value for 'cipher' option.
* Adjust tests.
* Add changelog fragment.
* Clarify that cipher is used only when passphrase is provided.
2024-08-30 08:49:20 +01:00
Felix Fontein
f0b8073ea5 Improve communication link description. 2024-08-15 21:40:07 +02:00
Felix Fontein
49f64aecee Improve communication info. (#792) 2024-08-12 17:05:24 +02:00
Andrew Klychkov
dc49cc6e26 README: Add Communication section with Forum information (#790)
* README: Add Communication section with Forum information

* Insert tag, remove category.

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
2024-08-12 12:53:29 +02:00
Felix Fontein
e42f8e0d0c Make pylint happy. (#789) 2024-08-07 14:48:58 +02:00
Felix Fontein
114a29f4ea Next expected release will be 2.22.0. 2024-07-21 16:11:49 +02:00
Felix Fontein
bb1cdef4c6 Release 2.21.1. 2024-07-21 15:48:08 +02:00
Felix Fontein
cd0444bd53 Prepare 2.21.1 release. 2024-07-21 15:14:40 +02:00
Felix Fontein
cb3f55076e Support InvalidityDate.invalidity_date_utc. (#730) 2024-07-21 13:04:13 +02:00
Felix Fontein
e1e60892a8 Fix PKCS#12 tests. (#787) 2024-07-21 13:02:04 +02:00
Felix Fontein
d509af540d Disable unit tests that fail with cryptography 43.0.0. (#786) 2024-07-21 12:11:34 +02:00
Felix Fontein
c8767ede77 The next expected release is 2.22.0. 2024-07-12 22:37:49 +02:00
Felix Fontein
f7c0a85c72 Release 2.21.0. 2024-07-12 22:17:24 +02:00
Felix Fontein
8935ab8fdc Reformat and re-order changelogs/changelog.yaml. 2024-07-11 22:44:23 +02:00
Felix Fontein
1f39b0ff2a Add missing changelog for #784. 2024-07-11 22:35:47 +02:00
G Derber
b02fb8e9a0 certificate_complete_chain: add ability to identify ed25519 complete chains (#777)
* Add ability to identify ed25519 complete chains.

* Add ability to identify ed448 complete chains.

* Formatting updates

* Remove unnecessary imports.

* Cleanup whitespace

* Fix algorithm names capitalization.
2024-07-11 22:25:16 +02:00
Felix Fontein
d50c3cc944 get_certificate: add get_certificate_chain option (#784)
* Implement get_certificate_chain option.

* Implement basic tests.

* Add compatibility for current Python 3.13 pre-releases.
2024-07-10 21:51:30 +02:00
Felix Fontein
4c26fada5e Polish docs. (#783) 2024-07-10 00:20:24 +02:00
Felix Fontein
d13d1868b6 Remove EOL'ed FreeBSD 13.2 from CI. (#781)
Apparently the packages are no longer available.
2024-07-08 22:44:14 +02:00
dependabot[bot]
6a0953b19f Bump fsfe/reuse-action from 3 to 4 (#780)
Bumps [fsfe/reuse-action](https://github.com/fsfe/reuse-action) from 3 to 4.
- [Release notes](https://github.com/fsfe/reuse-action/releases)
- [Commits](https://github.com/fsfe/reuse-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: fsfe/reuse-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-08 07:53:36 +02:00
dlehrman
6ba06f24ce Enable TLS/SSL CTX Options for the get_certificate Module (#779)
* Enable SSL CTX options for get_certificate

Signed-off-by: David Ehrman <dlehrman@liberty.edu>

* Support both str and int SSL CTX options, override defaults

Signed-off-by: David Ehrman <dlehrman@liberty.edu>

* Add changelog fragment

Signed-off-by: David Ehrman <dlehrman@liberty.edu>

* Resolve doc builder error

ssl_ctx_options can be a mix of str and int, but `elements: [ str, int ]` made the Ansible doc builder angry.

Signed-off-by: David Ehrman <dlehrman@liberty.edu>

* Set ssl_ctx_options version_added

Signed-off-by: David Ehrman <dlehrman@liberty.edu>

* Initial application of suggestions from code review

Working on completing application of suggestions

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

* Finish applying suggestions from code review

Signed-off-by: David Ehrman <dlehrman@liberty.edu>

* Documentation update

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

* Include value in fail output for wrong data type

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

* Handle invalid tls_ctx_option strings

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

* Minor documentation update

Signed-off-by: David Ehrman <dlehrman@liberty.edu>

---------

Signed-off-by: David Ehrman <dlehrman@liberty.edu>
Co-authored-by: Felix Fontein <felix@fontein.de>
2024-07-07 21:48:48 +02:00
Felix Fontein
577d86265e Prepare 2.21.0 release. 2024-07-07 20:11:55 +02:00
Felix Fontein
1c1b59b719 Add link to forum. (#778) 2024-07-05 22:33:00 +02:00
Felix Fontein
518847a92c CI: DSA SSH keys are no longer supported with OpenSSH 9.8p1 (#776)
* DSA SSH keys are no longer supported with OpenSSH 9.8p1.

* Add more compatibility tests.
2024-07-04 10:15:22 +02:00
Felix Fontein
aa30b4c803 Fix CI for CentOS 7. (#774) 2024-07-01 13:56:23 +02:00
Felix Fontein
a9dab608c7 Adjust docs publishing workflow. (#773)
Ref: https://github.com/ansible-community/github-docs-build/issues/92
2024-06-29 17:23:11 +02:00
Felix Fontein
e6643fd2dd Replace FreeBSD 14.0 with 14.1; add 14.0 for stable-2.17. (#772) 2024-06-21 21:38:19 +02:00
Felix Fontein
f58606b64d Add Python 3.13 to CI. (#768) 2024-06-18 23:08:54 +02:00
Felix Fontein
5e60bee9c0 Adjust CI matrix for ansible-core devel's ansible-test (#771)
* Adjust CI matrix for ansible-core devel's ansible-test.

* Don't install cryptography via pip on Ubuntu 24.04.

* Don't force-enable on Fedora.
2024-06-18 08:20:43 +02:00
Felix Fontein
33410b1d57 Removing Fedora 31 and 32 from CI. These images seem to no longer work. 2024-06-15 14:01:44 +02:00
Felix Fontein
e365ae3226 Use 2.9/2.10/2.11 from ansible-community/eol-ansible repo. (#769) 2024-06-15 13:49:41 +02:00
Felix Fontein
5f6e0095b0 Fix unit tests. (#767) 2024-06-13 21:33:36 +02:00
Felix Fontein
dc052bee21 Bump Azure test container to 6.0.0. (#764) 2024-06-10 20:41:22 +02:00
Felix Fontein
38849514f3 Stop building EE with CentOS Stream 8, which no longer has builds. (#763) 2024-06-04 07:40:46 +02:00
Felix Fontein
7810e2c3bf Remove usage of old ACME test container. (#760) 2024-05-20 16:11:35 +02:00
Felix Fontein
5d4cbbb038 The next expected release will be 2.21.0. 2024-05-20 12:15:59 +02:00
Felix Fontein
58a81374d6 Release 2.20.0. 2024-05-20 11:30:21 +02:00
Felix Fontein
c29c34bab2 Prepare 2.20.0. 2024-05-20 11:26:23 +02:00
Felix Fontein
b4452d4be1 From now on automatically add period to new plugins in changelog, and use FQCNs. (#759) 2024-05-20 08:44:11 +02:00
Felix Fontein
7fc3ad0263 Make sure the ACME inspect tests run with both backends. (#758) 2024-05-12 15:29:07 +02:00
Felix Fontein
65ea02a73d Pass codecov token to ansible-test-gh-action. (#755) 2024-05-11 21:29:25 +02:00
Felix Fontein
00d23753ca Revert "Revert all non-bugfixes merged since the last release."
This reverts commit 82251c2d80.
2024-05-11 17:05:03 +02:00
Felix Fontein
3d8c68e189 Next planned release is 2.20.0. 2024-05-11 17:05:03 +02:00
Felix Fontein
d7a0723a52 Release 2.19.1. 2024-05-11 16:43:18 +02:00
Felix Fontein
67bf3a7991 Prepare 2.19.1 bugfix release. 2024-05-11 16:10:21 +02:00
Felix Fontein
82251c2d80 Revert all non-bugfixes merged since the last release.
Revert "Fix documentation. (#751)"
Revert "ACME modules: simplify code, refactor argspec handling code, move csr/csr_content to own docs fragment (#750)"
Revert "Refactor and extend argument spec helper, use for ACME modules (#749)"
Revert "Avoid exception if certificate has no AKI in acme_certificate. (#748)"
Revert "ACME: improve acme_certificate docs, include cert_id in acme_certificate_renewal_info return value (#747)"
Revert "Add acme_certificate_renewal_info module (#746)"
Revert "Refactor time code, add tests, fix bug when parsing absolute timestamps that omit seconds (#745)"
Revert "Add tests for acme_certificate_deactivate_authz module. (#744)"
Revert "Create acme_certificate_deactivate_authz module (#741)"
Revert "acme_certificate: allow to request renewal of a certificate according to ARI (#739)"
Revert "Implement basic acme_ari_info module. (#732)"
Revert "Add function for retrieval of ARI information. (#738)"
Revert "acme module utils: add functions for parsing Retry-After header values and computation of ARI certificate IDs (#737)"
Revert "Implement certificate information retrieval code in the ACME backends. (#736)"
Revert "Split up the default acme docs fragment to allow modules ot not need account data. (#735)"

This reverts commits 5e59c5261e, aa82575a78,
f3c9cb7a8a, f82b335916, 553ab45f46,
59606d48ad, 0a15be1017, 9501a28a93,
d906914737, 33d278ad8f, 6d4fc589ae,
9614b09f7a, af5f4b57f8, c6fbe58382,
and afe7f7522c.
2024-05-11 16:07:53 +02:00
Felix Fontein
f43fa94549 x509_certificate: fix time idempotence (#754)
* Fix time idempotence.

* Lint and add changelog fragment.

* Add tests.

* Make sure 'ignore_timestamps: false' is passed for time idempotence tests; pass right private key for OwnCA tests
2024-05-11 16:04:41 +02:00
francescolovecchio
29ac3cbe81 ecs_certificate: allow to request renewal without csr (#740)
* renew request CSR validation

* Create 740-ecs_certificate-renewal-without-csr

* Rename 740-ecs_certificate-renewal-without-csr to 740-ecs_certificate-renewal-without-csr.yml

---------

Co-authored-by: flovecchio <flovecchio@sorint.com>
2024-05-09 20:24:48 +02:00
Felix Fontein
5e59c5261e Fix documentation. (#751) 2024-05-05 19:57:32 +02:00
Felix Fontein
aa82575a78 ACME modules: simplify code, refactor argspec handling code, move csr/csr_content to own docs fragment (#750)
* Fix bug in argspec module util.

* Move csr / csr_content to new docs fragment.

* Simplify code.

* Refactor ACME argspec creation. Add with_certificate argument for new CERTIFICATE docs fragment.
2024-05-05 14:37:52 +02:00
Felix Fontein
f3c9cb7a8a Refactor and extend argument spec helper, use for ACME modules (#749)
* Refactor argument spec helper.

* Remove superfluous comments.
2024-05-05 09:42:42 +00:00
Felix Fontein
f82b335916 Avoid exception if certificate has no AKI in acme_certificate. (#748)
Shouldn't happen since CA-issued certs should always have AKI,
but better be safe than sorry.
2024-05-05 09:43:29 +02:00
Felix Fontein
553ab45f46 ACME: improve acme_certificate docs, include cert_id in acme_certificate_renewal_info return value (#747)
* Use community.dns.quote_txt filter instead of regex replace to quote TXT entry value.

* Fix documentation of acme_certificate's challenge_data return value.

* Also return cert_id from acme_certificate_renewal_info module.

* The cert ID cannot be computed if the certificate has no AKI.

This happens with older Pebble versions, which are used when
testing against older ansible-core/-base/Ansible versions.

* Fix AKI extraction for older OpenSSL versions.
2024-05-04 23:38:57 +02:00
Felix Fontein
59606d48ad Add acme_certificate_renewal_info module (#746)
* Allow to provide cert_info object to get_renewal_info().

* Add acme_certificate_renewal_info module.

* Allow to provide value for 'now'.

* Actually append msg_append.

* Fix bug in module timestamp param parsing, and add tests.
2024-05-04 15:47:42 +02:00
Felix Fontein
0a15be1017 Refactor time code, add tests, fix bug when parsing absolute timestamps that omit seconds (#745)
* Add time module utils.

* Add time helpers to ACME backend.

* Add changelog fragment.

* ACME timestamp parser: do not choke on nanoseconds.
2024-05-03 22:25:39 +02:00
Felix Fontein
9501a28a93 Add tests for acme_certificate_deactivate_authz module. (#744) 2024-05-01 11:30:07 +02:00
Felix Fontein
d906914737 Create acme_certificate_deactivate_authz module (#741)
* Create acme_certificate_deactivate_authz module.

* Add ACME version check.
2024-05-01 10:32:03 +02:00
Felix Fontein
33d278ad8f acme_certificate: allow to request renewal of a certificate according to ARI (#739)
* Allow to request renewal of a certificate according to ARI in acme_certificate.

* Improve docs.

* Fix typo and use right object.

* Add warning.
2024-04-30 10:47:49 +02:00
Felix Fontein
6d4fc589ae Implement basic acme_ari_info module. (#732) 2024-04-30 08:47:24 +02:00
Felix Fontein
9614b09f7a Add function for retrieval of ARI information. (#738) 2024-04-29 23:37:55 +02:00
Felix Fontein
af5f4b57f8 acme module utils: add functions for parsing Retry-After header values and computation of ARI certificate IDs (#737)
* Implement Retry-After value parse.

* Add cert ID computation function.

* Add tests and links to MDN.
2024-04-29 23:06:35 +02:00
Felix Fontein
c6fbe58382 Implement certificate information retrieval code in the ACME backends. (#736) 2024-04-29 22:29:43 +02:00
Felix Fontein
afe7f7522c Split up the default acme docs fragment to allow modules ot not need account data. (#735) 2024-04-29 22:22:38 +02:00
Felix Fontein
0c62837296 crypto.math module utils: add some tests, fix quick_is_not_prime() for small primes (#733)
* Fix quick_is_not_prime() for small primes. Add some tests.

* Fix return value of convert_int_to_bytes(0, 0) on Python 2.

* Add some more test cases.

* Simplify the changelog and point out that these errors only happen for cases not happening in regular use.
2024-04-29 08:50:28 +02:00
Felix Fontein
d71637c77d Arch Linux switched to Python 3.12. (#731) 2024-04-28 15:20:03 +00:00
Felix Fontein
3899f79f97 Next expected release will be 2.20.0. 2024-04-20 12:06:08 +02:00
Felix Fontein
8ce0051f9b Release 2.19.0. 2024-04-20 11:48:34 +02:00
Felix Fontein
4be691da50 Include changelog in docsite. (#729) 2024-04-18 12:22:34 +02:00
Felix Fontein
8fe012cf09 Prepare 2.19.0 release. 2024-04-18 07:51:28 +02:00
Felix Fontein
27a9ff14fb Add x509_certificate_convert module. (#728) 2024-04-18 05:50:36 +00:00
Felix Fontein
ae548de502 Use timezone aware functionality when using cryptography >= 42.0.0 (#727)
* Use timezone aware functionality when using cryptography >= 42.0.0.

* Adjust OpenSSH certificate code to avoid functions deprecated in Python 3.12.

* Strip timezone info from isoformat() output.

* InvalidityDate.invalidity_date currently has no _utc variant.
2024-04-18 05:49:53 +00:00
Felix Fontein
1b75f1aa9c Add and use CryptoBackend.get_ordered_csr_identifiers(). (#725) 2024-04-13 22:43:14 +02:00
Felix Fontein
7e33398d5c ansible-core devel dropped support for Python 3.7. (#722) 2024-04-05 07:49:15 +02:00
Felix Fontein
50c2c4db29 CI: Add stable-2.17; copy ignore.txt files from 2.17 to 2.18; move stable-2.14 from AZP to GHA (#721)
* Add stable-2.17 to CI; copy ignore files from 2.17 to 2.18.

* Move stable-2.14 from AZP to GHA.
2024-04-03 08:32:16 +02:00
Felix Fontein
ee0ceea118 Move Alpine 3.18 docker to stable-2.16, add Alpine 3.19 docker, bump Alpine VM to 3.19. (#720) 2024-03-22 12:48:40 +01:00
Felix Fontein
b98cec74ae Add FreeBSD 13.3 and 14.0 for devel, move FreeBSD 13.2 to stable-2.16. (#719) 2024-03-21 21:58:37 +01:00
Felix Fontein
05cc5fe82b Add macOS 14.3 for devel, move 13.2 to stable-2.16. (#718) 2024-03-12 08:02:23 +01:00
dependabot[bot]
fad3c1352b Bump fsfe/reuse-action from 2 to 3 (#717)
Bumps [fsfe/reuse-action](https://github.com/fsfe/reuse-action) from 2 to 3.
- [Release notes](https://github.com/fsfe/reuse-action/releases)
- [Commits](https://github.com/fsfe/reuse-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: fsfe/reuse-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 06:23:55 +01:00
Felix Fontein
4167d2c4b3 Next expected release will be 2.19.0. 2024-02-25 21:32:06 +01:00
Felix Fontein
ff1504dc58 Release 2.18.0. 2024-02-25 20:57:38 +01:00
Felix Fontein
08adb6b297 Deprecate check mode behavior of pipe modules. (#714) 2024-02-25 17:00:37 +01:00
Felix Fontein
42ba0a88f4 Prepare 2.18.0. 2024-02-23 20:07:06 +01:00
Felix Fontein
1736602ce7 Allow to configure how serial numbers are provided to x509_crl. (#715) 2024-02-19 21:05:13 +01:00
Felix Fontein
6b1a3d6e68 Add conversion filters for serial numbers (#713)
* Refactoring.

* Add parse_filter and to_filter plugins.

* Mention filters when serial numbers are accepted or returned.
2024-02-18 21:27:48 +01:00
Steffen Gufler
51591891d3 luks_device: fix remove_keyslot not working when set to 0 and duplicate keys (#710)
* luks_device: fix remove_keyslot not working when set to 0

* luks_device: fix module outputting 'ok' when trying to add a key that is already present in another keyslot

* luks_device: fix breaking unit tests

* luks_device: Duplicate key test case code cleanup

* luks_device: Fix testing of LUKS passphrases when only testing one key slot

* luks_device: Fix testing of LUKS passphrases when only testing one key slot

* luks_device: Add changelog fragment for PR #710

* luks_device: Update changlog fragment
2024-02-11 12:23:21 +01:00
Felix Fontein
d1a229c255 Add MarkDown changelog and use it by default. (#708) 2024-02-09 13:08:12 +01:00
Felix Fontein
d9698a6eff Next expected release is 2.18.0. 2024-01-27 12:47:38 +01:00
Felix Fontein
37fed289e6 Release 2.17.1. 2024-01-27 10:44:08 +01:00
Felix Fontein
9ec8680936 Emit warning when consistency cannot be checked. (#705) 2024-01-27 10:39:13 +01:00
Felix Fontein
87af1f2761 Disable consistency checking of RSA keys for cryptography 42.0.0 which no longer gives access to the required function. (#702) 2024-01-26 17:47:46 +01:00
Felix Fontein
da30487119 Prepare 2.17.1 release. 2024-01-25 23:52:22 +01:00
Felix Fontein
b57aa4a2ca Fix openssl_dhparam. (#698) 2024-01-25 23:42:03 +01:00
Felix Fontein
a5f5ea1128 Next expected release is 2.18.0. 2024-01-21 09:29:10 +01:00
Felix Fontein
91dd7cd4dc Release 2.17.0. 2024-01-21 09:03:37 +01:00
Felix Fontein
2913826352 Prepare 2.17.0 release. 2024-01-21 08:46:32 +01:00
Felix Fontein
0bc15598d7 Simplifiy workflows. (#696) 2024-01-17 23:14:53 +01:00
Felix Fontein
fb3f68ca96 Use import galaxy workflow from https://github.com/ansible-collections/community.docker/pull/754. (#694) 2024-01-13 17:08:03 +01:00
0x00ace
a4edf22a9c add allow discard option for luks devices (#693)
* add allow discard option for luks devices

* Add allow_discards to perfomance tests

* Fix version for luks devices doc

* Update plugins/modules/luks_device.py

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

* add changelog fragment

* Update changelogs/fragments/693-allow-discards.yaml

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

* added allow_discards to the persistently stored option list

* allow_discards works with not only luks2 containers

* Update plugins/modules/luks_device.py

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

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
2024-01-13 09:34:07 +01:00
Felix Fontein
97e44c4ba5 Remove some Shippable specific code that trips latest shellcheck. (#692) 2024-01-04 22:46:46 +01:00
Felix Fontein
453adb5d04 Remove FreeBSD 12.4 from CI. (#690) 2023-12-31 13:51:54 +00:00
Felix Fontein
033b456b7a Add new error message. (#688) 2023-12-20 13:37:19 +01:00
dependabot[bot]
73dbb84fc6 Bump actions/setup-python from 4 to 5 (#686)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-11 06:56:23 +01:00
Felix Fontein
780fb28946 Next expected release is 2.17.0. 2023-12-09 11:24:42 +01:00
Felix Fontein
815ce43d17 Release 2.16.2. 2023-12-09 11:03:32 +01:00
Felix Fontein
170d837122 Increase retry count from 5 to 10. (#685) 2023-12-08 21:36:20 +01:00
Felix Fontein
b5269b25a3 Improve error reporting. (#684) 2023-12-08 20:57:49 +01:00
Felix Fontein
f12e814344 Deactivate FreeBSD 13.1 in CI. (#683) 2023-12-07 22:50:33 +01:00
Felix Fontein
5d5a21fddf Directly handle unexpected non-JSON results. (#682) 2023-12-07 22:26:04 +01:00
Felix Fontein
67f1d1129b Fix handling of non-existing ACME accounts with Digicert ACME endpoint (#681)
* Compatibility for DigiCert CA: also accept 404 instead of 400 for non-existing accounts.

* Add changelog fragment.

* Fix URL.
2023-12-07 22:25:54 +01:00
Felix Fontein
d9362a2ce9 Prepare 2.16.2 release. 2023-12-07 21:08:34 +01:00
Felix Fontein
4e5966e477 Next expected release is 2.17.0. 2023-12-04 22:52:42 +01:00
Felix Fontein
22e24f24c6 Release 2.16.1. 2023-12-04 21:49:56 +01:00
Felix Fontein
35b47f73f4 Fix version in galaxy.yml to 2.16.1. 2023-12-04 21:49:44 +01:00
Felix Fontein
9cc1731767 Revert "Release 2.17.0."
This reverts commit c592eaa35a.
2023-12-04 21:49:29 +01:00
Felix Fontein
c592eaa35a Release 2.17.0. 2023-12-04 21:49:01 +01:00
Felix Fontein
525a8a5df4 Prepare 2.16.1. 2023-12-04 21:35:41 +01:00
Felix Fontein
e4ba0861e5 Retry also on certain connection errors. (#680) 2023-12-04 21:34:51 +01:00
Felix Fontein
29cd0b3bde Fix bad expressions in tests. (#677)
ci_complete
2023-11-28 22:57:45 +01:00
Felix Fontein
f2ebae635a Remove Fedora 36 from CI. (#676) 2023-11-24 21:21:14 +01:00
Felix Fontein
75934cdd8c devel supports Fedora 39, and no longer Fedora 38. (#674) 2023-11-17 21:29:45 +01:00
Felix Fontein
cf1fe027dd Add rhel/9.3 for devel, remove rhel/9.2. (#673) 2023-11-15 21:55:20 +01:00
Felix Fontein
e9dbc1a5a5 Next release is expected to be 2.17.0. 2023-10-29 16:17:00 +01:00
Felix Fontein
6bd5eee9b0 Release 2.16.0. 2023-10-29 15:59:31 +01:00
Felix Fontein
fc707c7e31 Add changelog fragment for #664. 2023-10-29 10:55:12 +01:00
Felix Fontein
eba7e32df1 Due to a new feature, the next release will be 2.16.0. 2023-10-29 10:53:53 +01:00
Steffen Gufler
6504e67139 luks_device: add support for keyslots (#664)
* luks_device: add support for keyslots

* luks_device: replace python3 format strings with python2 format strings, remove print statements

* luks_device: add missing copyright information in keyslot integration test files

* luks_device: updated failing unit tests for keyslot support

* luks_device: improve detection of luks version

* luks_device: Update documentation on keyslot parameters, minor code improvements

* luks_device: improve validation of keyslot parameters, fix tests for systems that do not support luks2

* luks_device: correct spelling and errors in documentation and output, check all possible locations for LUKS2 header
2023-10-29 10:53:00 +01:00
Felix Fontein
428550165a Fix typos and FQCN (#669)
* Fix typos.

* Use FQCNs in examples.
2023-10-28 22:54:56 +02:00
Felix Fontein
a150e77507 Prepare 2.15.2 release. 2023-10-28 22:14:10 +02:00
Felix Fontein
d1299c11d6 Handle pyOpenSSL 23.3.0, which removed PKCS#12 support (at least partially). (#666) 2023-10-28 13:38:07 +00:00
Felix Fontein
fccc9d32ee macOS in CI seems to be very unreliable or even totally dead. (#665) 2023-10-22 18:05:21 +02:00
Felix Fontein
d63c195bff Emphasize that openssl_publickey doesn't support OpenSSH private keys. (#663) 2023-10-07 15:21:09 +02:00
Felix Fontein
e7515584b1 Latest OpenSSH's ssh-keygen defaults to ed25519 keys, no longer RSA. (#662) 2023-10-07 15:15:33 +02:00
Felix Fontein
0d010968e5 ansible-core devel drops support for Python 2.7 and 3.6. (#660) 2023-10-04 08:22:33 +02:00
Felix Fontein
5f4fc95c50 Fix Galaxy URLs. (#658) 2023-09-30 21:30:36 +02:00
Felix Fontein
b2a92ef0bf Add ansible-core 2.16 to the matrix. (#656) 2023-09-19 17:51:29 +02:00
dependabot[bot]
01cdc4a572 Bump actions/checkout from 3 to 4 (#655)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-11 06:00:41 +02:00
Felix Fontein
cdfc881b32 Next expected release is 2.16.0. 2023-08-22 17:16:26 +02:00
Felix Fontein
d7293aa1cd Release 2.15.1. 2023-08-22 06:54:54 +02:00
Felix Fontein
1e78918ad3 Prepare 2.15.1 release. 2023-08-21 20:51:04 +02:00
Felix Fontein
526b3c4393 Allow type to be missing. (#652) 2023-08-21 20:49:55 +02:00
Felix Fontein
5d2bfddc15 FreeBSD 13.0 and 12.3 are no longer availabe, bump versions and disable since these versions are already tested with stable-2.15. (#649) 2023-08-13 19:19:30 +02:00
Felix Fontein
5ac603bbcc Next expected release is 2.16.0. 2023-08-12 19:48:40 +02:00
Felix Fontein
e41a50af97 Release 2.15.0. 2023-08-12 18:10:13 +02:00
Felix Fontein
d3737f5ef7 Update release summary. 2023-08-12 17:15:09 +02:00
Felix Fontein
addbd067c8 openssh_* modules: check return code on ssh(-keygen) invocations; fail if comment cannot be updated (#646)
* Check return code on ssh(-keygen) invocations.

* openssh_cert: only check for errors if certificate should be present and module is not in check mode.

* Handle rc check for _get_private_key().

* Add changelog fragment.

* Only pass -o for comment updating when necessary.

* Now fails if comment cannot be updated.

This was silently ignored in the past.

* Avoid failing operation.
2023-08-12 17:14:00 +02:00
Felix Fontein
62c842548d Deprecate the default value 'false' of asn1_base64. (#600) 2023-08-12 12:23:37 +02:00
Kloppi313
5526fcac27 Update openssl_privatekey.py (#644)
added example for ECC
2023-08-08 13:40:24 +02:00
Felix Fontein
55c94eb5c0 Update content list in README. (#643) 2023-08-02 12:00:25 +02:00
Felix Fontein
e64d617de6 Prepare 2.15.0 release. 2023-08-02 11:23:22 +02:00
Felix Fontein
ba456c5eaf Add gpg_fingerprint lookup and filter (#639)
* Add gpg_fingerprint lookup.

* Work around problems on some CI targets.

* Use get_bin_path to find the gpg executable. Document that we need it.

* Improve and test error handling.

* Refactor (potentially) common code to module_utils and plugin_utils.

This will be useful to create a filter version of this, and further lookups, filters, and modules.

* Do not create a keyring when there isn't one.

* Fixups.

* Fix description.

* More fixes for lookup.

* Also add a gpg_fingerprint filter.

* Improve formulation.

Co-authored-by: Sandra McCann <samccann@redhat.com>

---------

Co-authored-by: Sandra McCann <samccann@redhat.com>
2023-08-02 11:16:34 +02:00
Felix Fontein
5e630ffe78 CI: ansible-core devel only supports Alpine 3.18 VMs, no longer Alpine 3.17 VMs (#642)
* ansible-core devel only supports Alpine 3.18 VMs, no longer Alpine 3.17 VMs.

* lsblk was moved to a separate package in Alpine 3.18.
2023-08-02 11:15:54 +02:00
Felix Fontein
9ae75d4840 Fix license disclaimer for some vendored Jinja2 code in tests. (#640) 2023-07-26 17:45:24 +02:00
Felix Fontein
78eeb1219a Move FreeBSD 12.4 from ansible-core devel to stable-2.15. (#641) 2023-07-20 20:35:42 +02:00
Felix Fontein
54b2163c56 Remove no longer needed ignore. (#638) 2023-07-15 12:40:58 +02:00
Felix Fontein
1ca0d2f21d Install and use Python 3.11 on RHEL UBI 9. (#637) 2023-07-12 19:24:51 +02:00
Felix Fontein
2a789f8b01 Disable EE with ansible-core devel for now until UBI 9 has Python 3.10 support. (#636) 2023-07-12 08:12:46 +02:00
Felix Fontein
cffba005f0 Next expected release is 2.15.0. 2023-06-27 21:21:08 +02:00
Felix Fontein
6c72734652 Release 2.14.1. 2023-06-27 18:03:55 +02:00
Felix Fontein
83af72a3bc Improve PEM identification. (#628) 2023-06-27 17:35:55 +02:00
Felix Fontein
ed6285e083 Remove Fedora 37 from devel; add Fedora 38. (#633) 2023-06-26 22:36:04 +02:00
Felix Fontein
57a8c7e652 Add Debian Bookworm to CI. (#631) 2023-06-24 16:29:21 +02:00
Felix Fontein
b40a1c54f7 Bump AZP container. (#629) 2023-06-24 12:14:01 +02:00
Felix Fontein
8fa4dc75c9 Prepare 2.14.1. 2023-06-24 10:02:16 +02:00
Felix Fontein
99d1521266 Use semantic markup (#626)
* Enable semantic markup.

* Use semantic markup.

* Break long lines.

* Add ignores.

* Use real option, not alias.
2023-06-24 10:00:56 +02:00
Felix Fontein
c78536dfeb Support for Ubuntu 20.04 VM was removed. (#625) 2023-06-21 22:36:23 +02:00
Felix Fontein
288dc5be2c Update README. 2023-06-19 23:19:04 +02:00
Felix Fontein
9ae28e2fab Add RHEL 8.7, 8.8, and 9.2 to CI. (#624) 2023-06-19 22:50:07 +02:00
Felix Fontein
f27b66baa3 Ubuntu 20.02 VM is being removed from ansible-core devel. (#623) 2023-06-16 06:16:34 +02:00
Felix Fontein
230f0b51f2 Next expected release is 2.15.0. 2023-06-15 13:34:02 +02:00
Felix Fontein
1f84d0a317 Release 2.14.0. 2023-06-15 12:52:42 +02:00
Felix Fontein
2f64d42855 Adjust release summary. 2023-06-15 12:52:15 +02:00
Marcin Słowikowski
9c07a8354e Added support for certificates in DER format for x509_certificate_info module (#622)
* Added support for DER format

* Updated description

* Adjusted description

The content of the certificate cannot be in DER format due to an input encoding problem in the Ansible module, but it works fine when reading the certificate from a file

* Update support.py

* Added der_support_enabled flag for DER-format support

* Added changelog fragment for #603

* Fixed typo

* Fixed missing import

* Resolved issues found by static code analysis

* Update plugins/module_utils/crypto/support.py

Committed suggested change

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

* Apply suggestions from code review

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

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
2023-06-15 12:51:14 +02:00
Felix Fontein
a7e9bb7618 Fix example. (#620) 2023-06-09 07:30:35 +02:00
Felix Fontein
ad118bbbd6 Prepare 2.14.0 release. 2023-06-09 06:10:06 +02:00
Felix Fontein
d823382732 Validate challenges in parallel instead of serially. (#617) 2023-06-09 06:04:34 +02:00
Felix Fontein
3a5d9129b2 ansible-core devel drops support for Python 3.5. (#618) 2023-06-06 21:28:59 +02:00
Felix Fontein
17702d1a76 acme_certificate: allow 'no challenge' (#615)
* Allow 'no challenge'.

* Fix undefined variable.
2023-06-05 20:54:07 +02:00
Felix Fontein
9305bfe190 Fix typo. (#616) 2023-06-04 20:12:35 +02:00
Felix Fontein
0d30a3793a Move ansible-core 2.12 to EOL CI (#609)
* https://github.com/ansible/ansible/pull/79734 has been merged and backported for all branches but stable-2.10 and stable-2.11.

* Move ansible-core 2.12 to EOL CI.
2023-05-29 17:01:04 +02:00
Felix Fontein
a402c485a3 Next expected release is 2.14.0. 2023-05-21 14:36:32 +02:00
Felix Fontein
05ad2e5008 Release 2.13.1. 2023-05-21 14:12:40 +02:00
Felix Fontein
e3bc22f7d5 Switch to Ansible Galaxy compatible requirements files for tests. (#607) 2023-05-21 13:33:19 +02:00
Felix Fontein
c703dd6056 Rewrite EE test workflows to use ansible-builder 3.0.0; fix EE dependencies (#606)
* Adjust EE tests to ansible-builder 3.0.0.

* Remove other CI workflows.

* Use docker instead of podman...

* Support Rocky Linux 9+.

* Add CentOS Stream 9 to EE tests.

* Fix installation of PyOpenSSL on CentOS/RHEL/Rocky.

* ansible-builder only attempts to install EPEL deps on CentOS.

* Make EPEL also available on Rocky Linux 9, even though ansible-builder will ignore it.

* Make sure cryptography is already installed.

* Try ansible-runner < 2.0.0 for CentOS Stream 8 / RHEL 8.

* Show more info.

* Start restricting transitive dependencies...

* Looks like PyOpenSSL is **broken** on CentOS Stream 9 + EPEL.

* ansible-builder will NOT work with Python 3.6.

use Python 3.9 on RHEL8 / CentOS Stream 8. Manually install cryptography and PyOpenSSL for Python 3.9 as well.

* PyOpenSSL isn't available for Python 3.8 or 3.9.

* Revert "Remove other CI workflows."

This reverts commit 3a9d125f45.

* Use podman instead of docker.

* Re-order bindep entries.

* python3-pyOpenSSL does not exist on RHEL/CentOS 6 and 7.
2023-05-21 12:43:14 +02:00
Felix Fontein
153de3ffef Prepare 2.13.1. 2023-05-21 08:39:06 +02:00
Felix Fontein
3bcc0db4fc Improve examples: use FQCNs and always add name: to tasks (#604)
* Improve examples: use FQCNs and always add name: to tasks.

* Improve formulation.

Co-authored-by: Don Naro <dnaro@redhat.com>

* Accidentally added a period.

---------

Co-authored-by: Don Naro <dnaro@redhat.com>
2023-05-15 21:41:18 +02:00
Felix Fontein
142403c6cb Arch Linux now uses Python 3.11. (#602) 2023-05-04 07:12:16 +02:00
Felix Fontein
a2d4554c78 Add FreeBSD 13.2, drop FreeBSD 12.2. (#601) 2023-05-03 22:04:40 +02:00
Felix Fontein
a89fd2733b Next expected release is 2.14.0. 2023-05-01 22:01:33 +02:00
Felix Fontein
39bba05a17 Release 2.13.0. 2023-05-01 21:27:25 +02:00
Felix Fontein
a8f27f93b7 Prepare 2.13.0. 2023-05-01 21:18:46 +02:00
David Zaslavsky
ce3299f106 Always generate a new key pair if the private key doesn't exist (#598)
* Always generate a new key pair if the private key doesn't exist (#597)

This commit updates `KeypairBackend._should_generate()` to first check
if the original private key named by the `path` argument exists, and
return True if it does not. This brings the code in line with
the documentation, which says that a new key will always be generated if
the key file doesn't already exist.

As an alternative to the approach implemented here, I also considered
only modifying the condition in the `fail` branch of the if statement,
but I thought that would not map as cleanly to the behavior specified in
the documentation, so doing it the way I did should make it easier to
check that the code is doing the right thing just by looking at it.
I also considered doing something to make the logic more similar to
`PrivateKeyBackend.needs_regeneration()` (the openssl version of this
functionality), because the two are supposed to be acting the same way,
but I thought that'd be going beyond the scope of just fixing this bug.
If it'd be useful to make both methods work the same way, someone can
refactor the code in a future commit.

* Test different regenerate values with nonexistent keys

This commit changes the test task that generates new keys to use each of
the different values for the `regenerate` argument, which will ensure
that the module is capable of generating a key when no previous key
exists regardless of the value of `regenerate`. Previously, the task
would always run with the `partial_idempotence` value, and that obscured
a bug (#597) that would occur when it was set to `fail`. The bug was
fixed in the previous commit.
2023-05-01 21:16:42 +02:00
Felix Fontein
c568923478 x509_crl: prepare releasing the mode option for AnsibleModule's use (#596)
* Prepare releasing the mode option for AnsibleModule's use.

* Update docs.
2023-04-29 20:54:24 +02:00
Felix Fontein
54eeb8d563 Next expected release is 2.13.0. 2023-04-16 20:14:03 +02:00
507 changed files with 46836 additions and 34018 deletions

31
.ansible-lint 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
skip_list:
# Ignore rules that make no sense:
- galaxy[tags]
- galaxy[version-incorrect]
- meta-runtime[unsupported-version]
- no-changed-when
- sanity[cannot-ignore] # some of the rules you cannot ignore actually MUST be ignored, like yamllint:unparsable-with-libyaml
- yaml # we're using yamllint ourselves
# To be checked and maybe fixed:
- fqcn[action]
- fqcn[action-core]
- ignore-errors
- jinja[spacing]
- key-order[task]
- name[casing]
- name[missing]
- name[play]
- name[template]
- no-free-form
- no-handler
- risky-file-permissions
- risky-shell-pipe
- var-naming[no-reserved]
- var-naming[pattern]
- var-naming[read-only]

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
@@ -46,7 +44,7 @@ variables:
resources: resources:
containers: containers:
- container: default - container: default
image: quay.io/ansible/azure-pipelines-test-container:3.0.0 image: quay.io/ansible/azure-pipelines-test-container:6.0.0
pool: Standard pool: Standard
@@ -61,54 +59,30 @@ stages:
targets: targets:
- name: Sanity - name: Sanity
test: 'devel/sanity/1' test: 'devel/sanity/1'
- name: Sanity Extra # Only on devel
test: 'devel/sanity/extra'
- name: Units - name: Units
test: 'devel/units/1' test: 'devel/units/1'
- stage: Ansible_2_15 - stage: Ansible_2_18
displayName: Sanity & Units 2.15 displayName: Sanity & Units 2.18
dependsOn: [] dependsOn: []
jobs: jobs:
- template: templates/matrix.yml - template: templates/matrix.yml
parameters: parameters:
targets: targets:
- name: Sanity - name: Sanity
test: '2.15/sanity/1' test: '2.18/sanity/1'
- name: Units - name: Units
test: '2.15/units/1' test: '2.18/units/1'
- stage: Ansible_2_14 - stage: Ansible_2_17
displayName: Sanity & Units 2.14 displayName: Sanity & Units 2.17
dependsOn: [] dependsOn: []
jobs: jobs:
- template: templates/matrix.yml - template: templates/matrix.yml
parameters: parameters:
targets: targets:
- name: Sanity - name: Sanity
test: '2.14/sanity/1' test: '2.17/sanity/1'
- name: Units - name: Units
test: '2.14/units/1' test: '2.17/units/1'
- stage: Ansible_2_13
displayName: Sanity & Units 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
targets:
- name: Sanity
test: '2.13/sanity/1'
- name: Units
test: '2.13/units/1'
- stage: Ansible_2_12
displayName: Sanity & Units 2.12
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
targets:
- name: Sanity
test: '2.12/sanity/1'
- name: Units
test: '2.12/units/1'
### Docker ### Docker
- stage: Docker_devel - stage: Docker_devel
displayName: Docker devel displayName: Docker devel
@@ -118,78 +92,46 @@ stages:
parameters: parameters:
testFormat: devel/linux/{0} testFormat: devel/linux/{0}
targets: targets:
- name: Fedora 37 - name: Fedora 41
test: fedora37 test: fedora41
- name: openSUSE 15 - name: Ubuntu 24.04
test: opensuse15 test: ubuntu2404
- name: Ubuntu 20.04 - name: Alpine 3.21
test: ubuntu2004 test: alpine321
groups:
- 1
- 2
- stage: Docker_2_18
displayName: Docker 2.18
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.18/linux/{0}
targets:
- name: Fedora 40
test: fedora40
- name: Ubuntu 24.04
test: ubuntu2404
- name: Alpine 3.20
test: alpine320
groups:
- 1
- 2
- stage: Docker_2_17
displayName: Docker 2.17
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.17/linux/{0}
targets:
- name: Fedora 39
test: fedora39
- name: Ubuntu 22.04 - name: Ubuntu 22.04
test: ubuntu2204 test: ubuntu2204
- name: Alpine 3 - name: Alpine 3.19
test: alpine3 test: alpine319
groups:
- 1
- 2
- stage: Docker_2_15
displayName: Docker 2.15
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.15/linux/{0}
targets:
- name: CentOS 7
test: centos7
groups:
- 1
- 2
- stage: Docker_2_14
displayName: Docker 2.14
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.14/linux/{0}
targets:
- name: Fedora 36
test: fedora36
groups:
- 1
- 2
- stage: Docker_2_13
displayName: Docker 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.13/linux/{0}
targets:
- name: openSUSE 15 py2
test: opensuse15py2
- name: Fedora 35
test: fedora35
- name: Fedora 34
test: fedora34
- name: Ubuntu 18.04
test: ubuntu1804
- name: Alpine 3
test: alpine3
groups:
- 1
- 2
- stage: Docker_2_12
displayName: Docker 2.12
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.12/linux/{0}
targets:
- name: CentOS 6
test: centos6
- name: Fedora 33
test: fedora33
groups: groups:
- 1 - 1
- 2 - 2
@@ -203,14 +145,12 @@ stages:
parameters: parameters:
testFormat: devel/linux-community/{0} testFormat: devel/linux-community/{0}
targets: targets:
- name: Debian Bookworm
test: debian-bookworm/3.11
- name: Debian Bullseye - name: Debian Bullseye
test: debian-bullseye/3.9 test: debian-bullseye/3.9
- name: ArchLinux - name: ArchLinux
test: archlinux/3.10 test: archlinux/3.13
- name: CentOS Stream 8 with Python 3.9
test: centos-stream8/3.9
- name: CentOS Stream 8 with Python 3.6
test: centos-stream8/3.6
groups: groups:
- 1 - 1
- 2 - 2
@@ -224,14 +164,14 @@ stages:
parameters: parameters:
testFormat: devel/{0} testFormat: devel/{0}
targets: targets:
- name: Alpine 3.17 - name: Alpine 3.21
test: alpine/3.17 test: alpine/3.21
- name: Fedora 37 - name: Fedora 41
test: fedora/37 test: fedora/41
- name: Ubuntu 20.04
test: ubuntu/20.04
- name: Ubuntu 22.04 - name: Ubuntu 22.04
test: ubuntu/22.04 test: ubuntu/22.04
- name: Ubuntu 24.04
test: ubuntu/24.04
groups: groups:
- vm - vm
- stage: Remote_devel - stage: Remote_devel
@@ -242,77 +182,46 @@ stages:
parameters: parameters:
testFormat: devel/{0} testFormat: devel/{0}
targets: targets:
- name: macOS 13.2 - name: macOS 15.3
test: macos/13.2 test: macos/15.3
- name: RHEL 9.1 - name: RHEL 9.5
test: rhel/9.1 test: rhel/9.5
- name: FreeBSD 12.4 - name: FreeBSD 14.2
test: freebsd/12.4 test: freebsd/14.2
- name: FreeBSD 13.1 - name: FreeBSD 13.5
test: freebsd/13.1 test: freebsd/13.5
groups: groups:
- 1 - 1
- 2 - 2
- stage: Remote_2_15 - stage: Remote_2_18
displayName: Remote 2.15 displayName: Remote 2.18
dependsOn: [] dependsOn: []
jobs: jobs:
- template: templates/matrix.yml - template: templates/matrix.yml
parameters: parameters:
testFormat: 2.15/{0} testFormat: 2.18/{0}
targets: targets:
- name: RHEL 7.9 - name: macOS 14.3
test: rhel/7.9 test: macos/14.3
- name: RHEL 9.4
test: rhel/9.4
- name: FreeBSD 14.1
test: freebsd/14.1
groups: groups:
- 1 - 1
- 2 - 2
- stage: Remote_2_14 - stage: Remote_2_17
displayName: Remote 2.14 displayName: Remote 2.17
dependsOn: [] dependsOn: []
jobs: jobs:
- template: templates/matrix.yml - template: templates/matrix.yml
parameters: parameters:
testFormat: 2.14/{0} testFormat: 2.17/{0}
targets: targets:
- name: macOS 12.0 - name: RHEL 9.3
test: macos/12.0 test: rhel/9.3
- name: RHEL 9.0 - name: FreeBSD 13.3
test: rhel/9.0 test: freebsd/13.3
- name: FreeBSD 12.3
test: freebsd/12.3
groups:
- 1
- 2
- stage: Remote_2_13
displayName: Remote 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.13/{0}
targets:
- name: RHEL 8.5
test: rhel/8.5
- name: FreeBSD 13.0
test: freebsd/13.0
groups:
- 1
- 2
- stage: Remote_2_12
displayName: Remote 2.12
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.12/{0}
targets:
# Not working anymore:
# - name: macOS 11.1
# test: macos/11.1
- name: RHEL 8.4
test: rhel/8.4
- name: FreeBSD 12.2
test: freebsd/12.2
groups: groups:
- 1 - 1
- 2 - 2
@@ -326,67 +235,39 @@ stages:
nameFormat: Python {0} nameFormat: Python {0}
testFormat: devel/generic/{0} testFormat: devel/generic/{0}
targets: targets:
- test: 2.7 - test: "3.8"
- test: 3.5 - test: "3.9"
- test: 3.6
- test: 3.7
# - test: 3.8
# - test: 3.9
# - test: "3.10"
- test: "3.11"
groups:
- 1
- 2
- stage: Generic_2_15
displayName: Generic 2.15
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Python {0}
testFormat: 2.15/generic/{0}
targets:
- test: "3.10" - test: "3.10"
- test: "3.11"
- test: "3.13"
groups: groups:
- 1 - 1
- 2 - 2
- stage: Generic_2_14 - stage: Generic_2_18
displayName: Generic 2.14 displayName: Generic 2.18
dependsOn: [] dependsOn: []
jobs: jobs:
- template: templates/matrix.yml - template: templates/matrix.yml
parameters: parameters:
nameFormat: Python {0} nameFormat: Python {0}
testFormat: 2.14/generic/{0} testFormat: 2.18/generic/{0}
targets: targets:
- test: 3.9 - test: "3.8"
- test: "3.13"
groups: groups:
- 1 - 1
- 2 - 2
- stage: Generic_2_13 - stage: Generic_2_17
displayName: Generic 2.13 displayName: Generic 2.17
dependsOn: [] dependsOn: []
jobs: jobs:
- template: templates/matrix.yml - template: templates/matrix.yml
parameters: parameters:
nameFormat: Python {0} nameFormat: Python {0}
testFormat: 2.13/generic/{0} testFormat: 2.17/generic/{0}
targets: targets:
- test: 3.8 - test: "3.7"
groups: - test: "3.12"
- 1
- 2
- stage: Generic_2_12
displayName: Generic 2.12
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Python {0}
testFormat: 2.12/generic/{0}
targets:
- test: 2.6
- test: 3.9
groups: groups:
- 1 - 1
- 2 - 2
@@ -397,26 +278,18 @@ stages:
condition: succeededOrFailed() condition: succeededOrFailed()
dependsOn: dependsOn:
- Ansible_devel - Ansible_devel
- Ansible_2_15 - Ansible_2_18
- Ansible_2_14 - Ansible_2_17
- Ansible_2_13
- Ansible_2_12
- Remote_devel_extra_vms - Remote_devel_extra_vms
- Remote_devel - Remote_devel
- Remote_2_15 - Remote_2_18
- Remote_2_14 - Remote_2_17
- Remote_2_13
- Remote_2_12
- Docker_devel - Docker_devel
- Docker_2_15 - Docker_2_18
- Docker_2_14 - Docker_2_17
- Docker_2_13
- Docker_2_12
- Docker_community_devel - Docker_community_devel
- Generic_devel - Generic_devel
- Generic_2_15 - Generic_2_18
- Generic_2_14 - Generic_2_17
- Generic_2_13
- Generic_2_12
jobs: jobs:
- template: templates/coverage.yml - template: templates/coverage.yml

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)

View File

@@ -50,11 +50,11 @@ jobs:
parameters: parameters:
jobs: jobs:
- ${{ if eq(length(parameters.groups), 0) }}: - ${{ if eq(length(parameters.groups), 0) }}:
- ${{ each target in parameters.targets }}:
- name: ${{ format(parameters.nameFormat, coalesce(target.name, target.test)) }}
test: ${{ format(parameters.testFormat, coalesce(target.test, target.name)) }}
- ${{ if not(eq(length(parameters.groups), 0)) }}:
- ${{ each group in parameters.groups }}:
- ${{ each target in parameters.targets }}: - ${{ each target in parameters.targets }}:
- name: ${{ format(format(parameters.nameGroupFormat, parameters.nameFormat), coalesce(target.name, target.test), group) }} - name: ${{ format(parameters.nameFormat, coalesce(target.name, target.test)) }}
test: ${{ format(format(parameters.testGroupFormat, parameters.testFormat), coalesce(target.test, target.name), group) }} test: ${{ format(parameters.testFormat, coalesce(target.test, target.name)) }}
- ${{ if not(eq(length(parameters.groups), 0)) }}:
- ${{ each group in parameters.groups }}:
- ${{ each target in parameters.targets }}:
- name: ${{ format(format(parameters.nameGroupFormat, parameters.nameFormat), coalesce(target.name, target.test), group) }}
test: ${{ format(format(parameters.testGroupFormat, parameters.testFormat), coalesce(target.test, target.name), group) }}

View File

@@ -14,37 +14,37 @@ parameters:
jobs: jobs:
- ${{ each job in parameters.jobs }}: - ${{ each job in parameters.jobs }}:
- job: test_${{ replace(replace(replace(job.test, '/', '_'), '.', '_'), '-', '_') }} - job: test_${{ replace(replace(replace(job.test, '/', '_'), '.', '_'), '-', '_') }}
displayName: ${{ job.name }} displayName: ${{ job.name }}
container: default container: default
workspace: workspace:
clean: all clean: all
steps: steps:
- checkout: self - checkout: self
fetchDepth: $(fetchDepth) fetchDepth: $(fetchDepth)
path: $(checkoutPath) path: $(checkoutPath)
- bash: .azure-pipelines/scripts/run-tests.sh "$(entryPoint)" "${{ job.test }}" "$(coverageBranches)" - bash: .azure-pipelines/scripts/run-tests.sh "$(entryPoint)" "${{ job.test }}" "$(coverageBranches)"
displayName: Run Tests displayName: Run Tests
- bash: .azure-pipelines/scripts/process-results.sh - bash: .azure-pipelines/scripts/process-results.sh
condition: succeededOrFailed() condition: succeededOrFailed()
displayName: Process Results displayName: Process Results
- bash: .azure-pipelines/scripts/aggregate-coverage.sh "$(Agent.TempDirectory)" - bash: .azure-pipelines/scripts/aggregate-coverage.sh "$(Agent.TempDirectory)"
condition: eq(variables.haveCoverageData, 'true') condition: eq(variables.haveCoverageData, 'true')
displayName: Aggregate Coverage Data displayName: Aggregate Coverage Data
- task: PublishTestResults@2 - task: PublishTestResults@2
condition: eq(variables.haveTestResults, 'true') condition: eq(variables.haveTestResults, 'true')
inputs: inputs:
testResultsFiles: "$(outputPath)/junit/*.xml" testResultsFiles: "$(outputPath)/junit/*.xml"
displayName: Publish Test Results displayName: Publish Test Results
- task: PublishPipelineArtifact@1 - task: PublishPipelineArtifact@1
condition: eq(variables.haveBotResults, 'true') condition: eq(variables.haveBotResults, 'true')
displayName: Publish Bot Results displayName: Publish Bot Results
inputs: inputs:
targetPath: "$(outputPath)/bot/" targetPath: "$(outputPath)/bot/"
artifactName: "Bot $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName)" artifactName: "Bot $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName)"
- task: PublishPipelineArtifact@1 - task: PublishPipelineArtifact@1
condition: eq(variables.haveCoverageData, 'true') condition: eq(variables.haveCoverageData, 'true')
displayName: Publish Coverage Data displayName: Publish Coverage Data
inputs: inputs:
targetPath: "$(Agent.TempDirectory)/coverage/" targetPath: "$(Agent.TempDirectory)/coverage/"
artifactName: "Coverage $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName)" artifactName: "Coverage $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName)"

13
.flake8 Normal file
View File

@@ -0,0 +1,13 @@
# 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>
[flake8]
extend-ignore = E203, E402, F401
count = true
# TODO: decrease this to ~10
max-complexity = 60
# black's max-line-length is 89, but it doesn't touch long string literals.
# Since ansible-test's limit is 160, let's use that here.
max-line-length = 160
statistics = true

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

@@ -0,0 +1,10 @@
# 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
# Reformat with black another time, this time without Python 2 compatibility
797bd8a6e2a6f4a37a89ecb15ca34ec88b33258d

View File

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

View File

@@ -1,197 +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
# For the comprehensive list of the inputs supported by the ansible-community/ansible-test-gh-action GitHub Action, see
# https://github.com/marketplace/actions/ansible-test
name: EOL CI
on:
# Run EOL CI against all pushes (direct commits, also merged PRs), Pull Requests
push:
branches:
- main
- stable-*
pull_request:
# Run EOL CI once per day (at 09:00 UTC)
schedule:
- cron: '0 9 * * *'
concurrency:
# Make sure there is at most one active run per PR, but do not cancel any non-PR runs
group: ${{ github.workflow }}-${{ (github.head_ref && github.event.number) || github.run_id }}
cancel-in-progress: true
jobs:
sanity:
name: EOL Sanity (Ⓐ${{ matrix.ansible }})
strategy:
matrix:
ansible:
- '2.9'
- '2.10'
- '2.11'
# Ansible-test on various stable branches does not yet work well with cgroups v2.
# 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:
- name: Perform sanity testing
uses: felixfontein/ansible-test-gh-action@main
with:
ansible-core-version: stable-${{ matrix.ansible }}
coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }}
pull-request-change-detection: 'true'
testing-type: sanity
units:
# Ansible-test on various stable branches does not yet work well with cgroups v2.
# 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 }})
strategy:
# As soon as the first unit test fails, cancel the others to free up the CI queue
fail-fast: true
matrix:
ansible:
- '2.9'
- '2.10'
- '2.11'
steps:
- name: >-
Perform unit testing against
Ansible version ${{ matrix.ansible }}
uses: felixfontein/ansible-test-gh-action@main
with:
ansible-core-version: stable-${{ matrix.ansible }}
coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }}
pull-request-change-detection: 'true'
testing-type: units
integration:
# Ansible-test on various stable branches does not yet work well with cgroups v2.
# 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 }})
strategy:
fail-fast: false
matrix:
ansible:
- ''
docker:
- ''
python:
- ''
target:
- ''
exclude:
- ansible: ''
include:
# 2.9
- ansible: '2.9'
docker: fedora31
python: ''
target: azp/posix/1/
- ansible: '2.9'
docker: fedora31
python: ''
target: azp/posix/2/
- ansible: '2.9'
docker: ubuntu1804
python: ''
target: azp/posix/1/
- ansible: '2.9'
docker: ubuntu1804
python: ''
target: azp/posix/2/
- ansible: '2.9'
docker: default
python: '2.7'
target: azp/generic/1/
- ansible: '2.9'
docker: default
python: '2.7'
target: azp/generic/2/
# 2.10
- ansible: '2.10'
docker: centos6
python: ''
target: azp/posix/1/
- ansible: '2.10'
docker: centos6
python: ''
target: azp/posix/2/
- ansible: '2.10'
docker: default
python: '3.6'
target: azp/generic/1/
- ansible: '2.10'
docker: default
python: '3.6'
target: azp/generic/2/
# 2.11
- ansible: '2.11'
docker: fedora32
python: ''
target: azp/posix/1/
- ansible: '2.11'
docker: fedora32
python: ''
target: azp/posix/2/
- ansible: '2.11'
docker: alpine3
python: ''
target: azp/posix/1/
- ansible: '2.11'
docker: alpine3
python: ''
target: azp/posix/2/
- ansible: '2.11'
docker: default
python: '3.8'
target: azp/generic/1/
- ansible: '2.11'
docker: default
python: '3.8'
target: azp/generic/2/
steps:
- name: >-
Perform integration testing against
Ansible version ${{ matrix.ansible }}
under Python ${{ matrix.python }}
uses: felixfontein/ansible-test-gh-action@main
with:
ansible-core-version: stable-${{ matrix.ansible }}
coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }}
docker-image: ${{ matrix.docker }}
integration-continue-on-error: 'false'
integration-diff: 'false'
integration-retry-on-error: 'true'
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.general.git ../../community/general
pull-request-change-detection: 'true'
target: ${{ matrix.target }}
target-python-version: ${{ matrix.python }}
testing-type: integration

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]
@@ -38,12 +38,15 @@ jobs:
if: github.repository == 'ansible-collections/community.crypto' if: github.repository == 'ansible-collections/community.crypto'
permissions: permissions:
contents: write contents: write
pages: write
id-token: write
needs: [build-docs] needs: [build-docs]
name: Publish Ansible Docs name: Publish Ansible Docs
uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-publish-gh-pages.yml@main uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-publish-gh-pages.yml@main
with: with:
artifact-name: ${{ needs.build-docs.outputs.artifact-name }} artifact-name: ${{ needs.build-docs.outputs.artifact-name }}
action: ${{ (github.event.action == 'closed' || needs.build-docs.outputs.changed != 'true') && 'teardown' || 'publish' }} action: ${{ (github.event.action == 'closed' || needs.build-docs.outputs.changed != 'true') && 'teardown' || 'publish' }}
publish-gh-pages-branch: true
secrets: secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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
@@ -43,10 +43,13 @@ jobs:
if: github.repository == 'ansible-collections/community.crypto' if: github.repository == 'ansible-collections/community.crypto'
permissions: permissions:
contents: write contents: write
pages: write
id-token: write
needs: [build-docs] needs: [build-docs]
name: Publish Ansible Docs name: Publish Ansible Docs
uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-publish-gh-pages.yml@main uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-publish-gh-pages.yml@main
with: with:
artifact-name: ${{ needs.build-docs.outputs.artifact-name }} artifact-name: ${{ needs.build-docs.outputs.artifact-name }}
publish-gh-pages-branch: true
secrets: secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -4,17 +4,17 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
name: execution environment name: execution environment
on: 'on':
# Run CI against all pushes (direct commits, also merged PRs), Pull Requests # Run CI against all pushes (direct commits, also merged PRs), Pull Requests
push: push:
branches: branches:
- main - main
- stable-* - stable-*
pull_request: pull_request:
# Run CI once per day (at 04:45 UTC) # Run CI once per day (at 09:00 UTC)
# This ensures that even if there haven't been commits that we are still testing against latest version of ansible-builder # This ensures that even if there haven't been commits that we are still testing against latest version of ansible-builder
schedule: schedule:
- cron: '45 4 * * *' - cron: '0 9 * * *'
env: env:
NAMESPACE: community NAMESPACE: community
@@ -22,25 +22,66 @@ env:
jobs: jobs:
build: build:
name: Build and test EE (${{ matrix.runner_tag }}) name: Build and test EE (${{ matrix.name }})
strategy: strategy:
fail-fast: false
matrix: matrix:
runner_tag: name:
- devel - ''
- stable-2.12-latest ansible_core:
- stable-2.11-latest - ''
- stable-2.9-latest 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.12 python3.12-pip python3.12-wheel python3.12-cryptography
python_path: "/usr/bin/python3.12"
base_image: docker.io/redhat/ubi9:latest
pre_base: '"#"'
- name: ansible-core 2.17 @ Rocky Linux 9
ansible_core: https://github.com/ansible/ansible/archive/stable-2.17.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: quay.io/rockylinux/rockylinux:9
pre_base: RUN dnf install -y epel-release
- name: ansible-core 2.18 @ CentOS Stream 9
ansible_core: https://github.com/ansible/ansible/archive/stable-2.18.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: quay.io/centos/centos:stream9
pre_base: '"#"'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
path: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }} path: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: '3.10' python-version: '3.11'
- name: Install ansible-builder and ansible-navigator - name: Install ansible-builder and ansible-navigator
run: pip install ansible-builder ansible-navigator run: pip install ansible-builder ansible-navigator
@@ -69,16 +110,31 @@ jobs:
- name: Create files for building execution environment - name: Create files for building execution environment
run: | run: |
COLLECTION_FILENAME="$(ls "${{ env.NAMESPACE }}-${{ env.COLLECTION_NAME }}"-*.tar.gz)" COLLECTION_FILENAME="$(ls "${NAMESPACE}-${COLLECTION_NAME}"-*.tar.gz)"
# EE config # EE config
cat > execution-environment.yml <<EOF cat > execution-environment.yml <<EOF
--- ---
version: 1 version: 3
build_arg_defaults:
EE_BASE_IMAGE: 'quay.io/ansible/ansible-runner:${{ matrix.runner_tag }}'
dependencies: dependencies:
ansible_core:
package_pip: ${{ matrix.ansible_core }}
ansible_runner:
package_pip: ${{ matrix.ansible_runner }}
galaxy: requirements.yml 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 EOF
echo "::group::execution-environment.yml" echo "::group::execution-environment.yml"
cat execution-environment.yml cat execution-environment.yml
@@ -88,26 +144,29 @@ jobs:
cat > requirements.yml <<EOF cat > requirements.yml <<EOF
--- ---
collections: collections:
- name: ${COLLECTION_FILENAME} - name: src/${COLLECTION_FILENAME}
type: file type: file
EOF EOF
echo "::group::requirements.yml" echo "::group::requirements.yml"
cat requirements.yml cat requirements.yml
echo "::endgroup::" echo "::endgroup::"
- name: Build image based on ${{ matrix.runner_tag }} - name: Build image based on ${{ matrix.base_image }}
run: | run: |
mkdir -p context/_build/ ansible-builder build --verbosity 3 --tag test-ee:latest --container-runtime podman
cp "${{ env.NAMESPACE }}-${{ env.COLLECTION_NAME }}"-*.tar.gz context/_build/
ansible-builder build -v 3 -t test-ee:latest --container-runtime=podman - name: Show images
run: podman image ls
- name: Run basic tests - name: Run basic tests
run: > run: >
ansible-navigator run ansible-navigator run
--mode stdout --mode stdout
--container-engine podman
--pull-policy never --pull-policy never
--set-environment-variable ANSIBLE_PRIVATE_ROLE_VARS=true --set-environment-variable ANSIBLE_PRIVATE_ROLE_VARS=true
--execution-environment-image test-ee:latest --execution-environment-image test-ee:latest
-v -v
all.yml all.yml
${{ matrix.extra_vars }}
working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}/tests/ee working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}/tests/ee

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

@@ -0,0 +1,28 @@
---
# 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

View File

@@ -1,34 +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]
pull_request:
branches: [main]
# 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@v3
- name: Install dependencies
run: |
pip install reuse
- name: Check REUSE compliance (except some PEM files)
run: |
rm -f tests/integration/targets/*/files/*.pem
rm -f tests/integration/targets/*/files/roots/*.pem
reuse lint

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

7
.isort.cfg Normal file
View File

@@ -0,0 +1,7 @@
# 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
[isort]
profile=black
lines_after_imports = 2

19
.mypy.ini Normal file
View File

@@ -0,0 +1,19 @@
# 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
[mypy]
# check_untyped_defs = True
# disallow_untyped_defs = True -- not yet feasible
# strict = True -- only try to enable once everything is typed
strict_equality = True
[mypy-ansible.*]
# ansible-core has no typing information
# ignore_missing_imports = True
follow_untyped_imports = True
[mypy-ansible_collections.community.internal_test_tools.*]
# community.internal_test_tools has no typing information
ignore_missing_imports = True

592
.pylintrc Normal file
View File

@@ -0,0 +1,592 @@
# 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>
[MAIN]
# Clear in-memory caches upon conclusion of linting. Useful if running pylint
# in a server-like mode.
clear-cache-post-run=no
# Load and enable all available extensions. Use --list-extensions to see a list
# all available extensions.
#enable-all-extensions=
# Specify a score threshold under which the program will exit with error.
fail-under=10
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
# avoid hangs.
jobs=0
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.7
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# In verbose mode, extra non-checker-related info will be displayed.
#verbose=
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style. If left empty, argument names will be checked with the set
# naming style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style. If left empty, attribute names will be checked with the set naming
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style. If left empty, class attribute names will be checked
# with the set naming style.
#class-attribute-rgx=
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style. If left empty, class constant names will be checked with
# the set naming style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style. If left empty, class names will be checked with the set naming style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style. If left empty, constant names will be checked with the set naming
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style. If left empty, function names will be checked with the set
# naming style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style. If left empty, inline iteration names will be checked
# with the set naming style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style. If left empty, method names will be checked with the set naming style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style. If left empty, module names will be checked with the set naming style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Regular expression matching correct type alias names. If left empty, type
# alias names will be checked with the set naming style.
#typealias-rgx=
# Regular expression matching correct type variable names. If left empty, type
# variable names will be checked with the set naming style.
#typevar-rgx=
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style. If left empty, variable names will be checked with the set
# naming style.
#variable-rgx=
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
asyncSetUp,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# List of regular expressions of class ancestor names to ignore when counting
# public methods (see R0903)
exclude-too-few-public-methods=
# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of positional arguments for function / method.
max-positional-arguments=5
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[EXCEPTIONS]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions=builtins.BaseException,builtins.Exception
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=160
# Maximum number of lines in a module.
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow explicit reexports by alias from a package __init__.
allow-reexport-from-package=no
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
# UNDEFINED.
confidence=HIGH,
CONTROL_FLOW,
INFERENCE,
INFERENCE_FAILURE,
UNDEFINED
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=raw-checker-failed,
bad-inline-option,
deprecated-pragma,
duplicate-code,
file-ignored,
import-outside-toplevel,
missing-class-docstring,
missing-function-docstring,
missing-module-docstring,
locally-disabled,
suppressed-message,
use-implicit-booleaness-not-comparison,
use-implicit-booleaness-not-comparison-to-string,
use-implicit-booleaness-not-comparison-to-zero,
superfluous-parens,
too-few-public-methods,
too-many-arguments,
too-many-boolean-expressions,
too-many-branches,
too-many-function-args,
too-many-instance-attributes,
too-many-lines,
too-many-locals,
too-many-nested-blocks,
too-many-positional-arguments,
too-many-return-statements,
too-many-statements,
ungrouped-imports,
useless-parent-delegation,
wrong-import-order,
wrong-import-position,
# To clean up:
broad-exception-caught,
broad-exception-raised,
fixme,
invalid-name,
unused-argument,
# Cannot remove yet due to inadequacy of rules
inconsistent-return-statements, # doesn't notice that fail_json() does not return
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=
[METHOD_ARGS]
# List of qualified names (i.e., library.method) which require a timeout
# parameter e.g. 'requests.api.get,requests.api.post'
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
notes-rgx=
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
# Let 'consider-using-join' be raised when the separator to join on would be
# non-empty (resulting in expected fixes of the type: ``"- " + " -
# ".join(items)``)
suggest-join-with-non-empty-separator=yes
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
# 'convention', and 'info' which contain the number of messages in each
# category, as well as 'statement' which is the total number of statements
# analyzed. This score is used by the global evaluation report (RP0004).
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
msg-template=
# Set the output format. Available formats are: text, parseable, colorized,
# json2 (improved json format), json (old json format) and msvs (visual
# studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
#output-format=
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[SIMILARITIES]
# Comments are removed from the similarity computation
ignore-comments=yes
# Docstrings are removed from the similarity computation
ignore-docstrings=yes
# Imports are removed from the similarity computation
ignore-imports=yes
# Signatures are removed from the similarity computation
ignore-signatures=yes
# Minimum lines number of a similarity.
min-similarity-lines=4
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. No available dictionaries : You need to install
# both the python package and the system dependency for enchant to work.
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear at the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of symbolic message names to ignore for Mixin members.
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# Regex pattern to define which classes are considered mixins.
mixin-class-rgx=.*[Mm]ixin
# List of decorators that change the signature of a decorated function.
signature-mutators=
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io

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

1729
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,564 @@ Community Crypto Release Notes
.. contents:: Topics .. contents:: Topics
v3.0.0-a1
=========
Release Summary
---------------
First pre-release for community.crypto 3.0.0.
This release drops compatibility for ansible-core before 2.17, for Python before 3.7, and for cryptography before 3.3.
Minor Changes
-------------
- No longer provide cryptography's ``backend`` parameter. This will break with cryptography < 3.1 (https://github.com/ansible-collections/community.crypto/pull/878).
- On cryptography 36.0.0+, always use ``public_bytes()`` for X.509 extension objects instead of using cryptography internals to obtain DER value of extension (https://github.com/ansible-collections/community.crypto/pull/878).
- Python code modernization: add type hints and type checking (https://github.com/ansible-collections/community.crypto/pull/885).
- Python code modernization: avoid unnecessary string conversion (https://github.com/ansible-collections/community.crypto/pull/880).
- Python code modernization: avoid using ``six`` (https://github.com/ansible-collections/community.crypto/pull/884).
- Python code modernization: remove Python 3 specific code (https://github.com/ansible-collections/community.crypto/pull/877).
- Python code modernization: update ``__future__`` imports, remove Python 2 specific boilerplates (https://github.com/ansible-collections/community.crypto/pull/876).
- Python code modernization: use ``unittest.mock`` instead of ``ansible_collections.community.internal_test_tools.tests.unit.compat.mock`` (https://github.com/ansible-collections/community.crypto/pull/881).
- Python code modernization: use f-strings instead of ``%`` and ``str.format()`` (https://github.com/ansible-collections/community.crypto/pull/875).
- Remove ``backend`` parameter from internal code whenever possible (https://github.com/ansible-collections/community.crypto/pull/883).
- Remove various compatibility code for cryptography < 3.3 (https://github.com/ansible-collections/community.crypto/pull/878).
- Remove vendored copy of ``distutils.version`` in favor of vendored copy included with ansible-core 2.12+ (https://github.com/ansible-collections/community.crypto/pull/371).
- acme_* modules - improve parsing of ``Retry-After`` reply headers in regular ACME requests (https://github.com/ansible-collections/community.crypto/pull/890).
- action_module plugin utils - remove compatibility with older ansible-core/ansible-base/Ansible versions (https://github.com/ansible-collections/community.crypto/pull/872).
- x509_certificate, x509_certificate_pipe - the ``ownca_version`` and ``selfsigned_version`` parameters explicitly only allow the value ``3``. The module already failed for other values in the past, now this is validated as part of the module argument spec (https://github.com/ansible-collections/community.crypto/pull/890).
Breaking Changes / Porting Guide
--------------------------------
- All doc_fragments are now private to the collection and must not be used from other collections or unrelated plugins/modules. Breaking changes in these can happen at any time, even in bugfix releases (https://github.com/ansible-collections/community.crypto/pull/898).
- All module_utils and plugin_utils are now private to the collection and must not be used from other collections or unrelated plugins/modules. Breaking changes in these can happen at any time, even in bugfix releases (https://github.com/ansible-collections/community.crypto/pull/887).
- Ignore value of ``select_crypto_backend`` for all modules except acme_* and ..., and always assume the value ``auto``. This ensures that the ``cryptography`` version is always checked (https://github.com/ansible-collections/community.crypto/pull/883).
- The validation for relative timestamps is now more strict. A string starting with ``+`` or ``-`` must be valid, otherwise validation will fail. In the past such strings were often silently ignored, and in many cases the code which triggered the validation was not able to handle no result (https://github.com/ansible-collections/community.crypto/pull/885).
- acme.certificates module utils - the ``retrieve_acme_v1_certificate()`` helper function has been removed (https://github.com/ansible-collections/community.crypto/pull/873).
- get_certificate - the default for ``asn1_base64`` changed from ``false`` to ``true`` (https://github.com/ansible-collections/community.crypto/pull/873).
- x509_crl - the ``mode`` parameter no longer denotes the update mode, but the CRL file mode. Use ``crl_mode`` instead for the update mode (https://github.com/ansible-collections/community.crypto/pull/873).
Deprecated Features
-------------------
- acme_certificate - deprecate the ``agreement`` option which has no more effect. It will be removed from community.crypto 4.0.0 (https://github.com/ansible-collections/community.crypto/pull/891).
- openssl_pkcs12 - deprecate the ``maciter_size`` option which has no more effect. It will be removed from community.crypto 4.0.0 (https://github.com/ansible-collections/community.crypto/pull/891).
Removed Features (previously deprecated)
----------------------------------------
- The collection no longer supports cryptography < 3.3 (https://github.com/ansible-collections/community.crypto/pull/878, https://github.com/ansible-collections/community.crypto/pull/882).
- acme.acme module utils - the ``get_default_argspec()`` function has been removed. Use ``create_default_argspec()`` instead (https://github.com/ansible-collections/community.crypto/pull/873).
- acme.backends module utils - the methods ``get_ordered_csr_identifiers()`` and ``get_cert_information()`` of ``CryptoBackend`` now must be implemented (https://github.com/ansible-collections/community.crypto/pull/873).
- acme.documentation docs fragment - the ``documentation`` docs fragment has been removed. Use both the ``basic`` and ``account`` docs fragments in ``acme`` instead (https://github.com/ansible-collections/community.crypto/pull/873).
- acme_* modules - support for ACME v1 has been removed (https://github.com/ansible-collections/community.crypto/pull/873).
- community.crypto no longer supports Ansible 2.9, ansible-base 2.10, and ansible-core versions 2.11, 2.12, 2.13, 2.14, 2.15, and 2.16. While content from this collection might still work with some older versions of ansible-core, it will not work with any Python version before 3.7 (https://github.com/ansible-collections/community.crypto/pull/870).
- crypto.basic module utils - remove ``CRYPTOGRAPHY_HAS_*`` flags. All tested features are supported since cryptography 3.0 (https://github.com/ansible-collections/community.crypto/pull/878).
- crypto.cryptography_support module utils - remove ``cryptography_serial_number_of_cert()`` helper function (https://github.com/ansible-collections/community.crypto/pull/878).
- crypto.module_backends.common module utils - this module utils has been removed. Use the ``argspec`` module utils instead (https://github.com/ansible-collections/community.crypto/pull/873).
- crypto.support module utils - remove ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/874).
- execution environment dependencies - remove PyOpenSSL dependency (https://github.com/ansible-collections/community.crypto/pull/874).
- openssl_csr_pipe - the module now ignores check mode and will always behave as if check mode is not active (https://github.com/ansible-collections/community.crypto/pull/873).
- openssl_pkcs12 - support for the ``pyopenssl`` backend has been removed (https://github.com/ansible-collections/community.crypto/pull/873).
- openssl_privatekey_pipe - the module now ignores check mode and will always behave as if check mode is not active (https://github.com/ansible-collections/community.crypto/pull/873).
- time module utils - remove ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/874).
- x509_certificate_pipe - the module now ignores check mode and will always behave as if check mode is not active (https://github.com/ansible-collections/community.crypto/pull/873).
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
=======
Release Summary
---------------
New feature and bugfix release with multiple new modules. It also deprecates support for older ansible-core and Python versions.
Minor Changes
-------------
- acme_certificate - add options ``order_creation_error_strategy`` and ``order_creation_max_retries`` which allow to configure the error handling behavior if creating a new ACME order fails. This is particularly important when using the ``include_renewal_cert_id`` option, and the default value ``auto`` for ``order_creation_error_strategy`` tries to gracefully handle related errors (https://github.com/ansible-collections/community.crypto/pull/842).
- acme_certificate - allow to chose a profile for certificate generation, in case the CA supports this using Internet-Draft `draft-aaron-acme-profiles <https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/>`__ (https://github.com/ansible-collections/community.crypto/pull/835).
- acme_certificate_renewal_info - add ``exists`` and ``parsable`` return values and ``treat_parsing_error_as_non_existing`` option (https://github.com/ansible-collections/community.crypto/pull/838).
Deprecated Features
-------------------
- Support for ansible-core 2.11, 2.12, 2.13, 2.14, 2.15, and 2.16 is deprecated, and will be removed in the next major release (community.crypto 3.0.0). Some modules might still work with some of these versions afterwards, but we will no longer keep compatibility code that was needed to support them. Note that this means that support for all Python versions before 3.7 will be dropped, also on the target side (https://github.com/ansible-collections/community.crypto/issues/559, https://github.com/ansible-collections/community.crypto/pull/839).
- Support for cryptography < 3.4 is deprecated, and will be removed in the next major release (community.crypto 3.0.0). Some modules might still work with older versions of cryptography, but we will no longer keep compatibility code that was needed to support them (https://github.com/ansible-collections/community.crypto/issues/559, https://github.com/ansible-collections/community.crypto/pull/839).
Bugfixes
--------
- crypto_info - when running the module on Fedora 41 with ``cryptography`` installed from the package repository, the module crashed apparently due to some elliptic curves being removed from libssl against which cryptography is running, which cryptography did not expect (https://github.com/ansible-collections/community.crypto/pull/834).
New Modules
-----------
- community.crypto.acme_certificate_order_create - Create an ACME v2 order.
- community.crypto.acme_certificate_order_finalize - Finalize an ACME v2 order.
- community.crypto.acme_certificate_order_info - Obtain information for an ACME v2 order.
- community.crypto.acme_certificate_order_validate - Validate authorizations of an ACME v2 order.
v2.23.0
=======
Release Summary
---------------
Feature release.
Minor Changes
-------------
- acme_certificate - add compatibility for ACME CAs that are not fully RFC8555 compliant and do not provide ``challenges`` in authz objects (https://github.com/ansible-collections/community.crypto/issues/824, https://github.com/ansible-collections/community.crypto/pull/832).
- luks_device - allow to provide passphrases base64-encoded (https://github.com/ansible-collections/community.crypto/issues/827, https://github.com/ansible-collections/community.crypto/pull/829).
- x509_certificate_convert - add new option ``verify_cert_parsable`` which allows to check whether the certificate can actually be parsed (https://github.com/ansible-collections/community.crypto/issues/809, https://github.com/ansible-collections/community.crypto/pull/830).
Deprecated Features
-------------------
- openssl_pkcs12 - the PyOpenSSL based backend is deprecated and will be removed from community.crypto 3.0.0. From that point on you need cryptography 3.0 or newer to use this module (https://github.com/ansible-collections/community.crypto/issues/667, https://github.com/ansible-collections/community.crypto/pull/831).
v2.22.3
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- acme_* modules - when using the OpenSSL backend, explicitly use the UTC timezone in Python code (https://github.com/ansible-collections/community.crypto/pull/811).
- time module utils - fix conversion of naive ``datetime`` objects to UNIX timestamps for Python 3 (https://github.com/ansible-collections/community.crypto/issues/808, https://github.com/ansible-collections/community.crypto/pull/810).
v2.22.2
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- acme_certificate - fix authorization failure when CSR contains SANs with mixed case (https://github.com/ansible-collections/community.crypto/pull/803).
v2.22.1
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- acme_* modules - when querying renewal information, make sure to insert a slash between the base URL and the certificate identifier (https://github.com/ansible-collections/community.crypto/issues/801, https://github.com/ansible-collections/community.crypto/pull/802).
- various modules - pass absolute paths to ``module.atomic_move()`` (https://github.com/ansible/ansible/issues/83950, https://github.com/ansible-collections/community.crypto/pull/799).
v2.22.0
=======
Release Summary
---------------
Feature release.
Minor Changes
-------------
- openssl_privatekey, openssl_privatekey_pipe - add default value ``auto`` for ``cipher`` option, which happens to be the only supported value for this option anyway. Therefore it is no longer necessary to specify ``cipher=auto`` when providing ``passphrase`` (https://github.com/ansible-collections/community.crypto/issues/793, https://github.com/ansible-collections/community.crypto/pull/794).
v2.21.1
=======
Release Summary
---------------
Maintenance release.
Bugfixes
--------
- When using cryptography >= 43.0.0, use offset-aware ``datetime.datetime`` objects (with timezone UTC) instead of offset-naive UTC timestamps for the ``InvalidityDate`` X.509 CRL extension (https://github.com/ansible-collections/community.crypto/issues/726, https://github.com/ansible-collections/community.crypto/pull/730).
v2.21.0
=======
Release Summary
---------------
Feature release.
Minor Changes
-------------
- certificate_complete_chain - add ability to identify Ed25519 and Ed448 complete chains (https://github.com/ansible-collections/community.crypto/pull/777).
- get_certificate - adds ``tls_ctx_options`` option for specifying SSL CTX options (https://github.com/ansible-collections/community.crypto/pull/779).
- get_certificate - allow to obtain the certificate chain sent by the server, and the one used for validation, with the new ``get_certificate_chain`` option. Note that this option only works if the module is run with Python 3.10 or newer (https://github.com/ansible-collections/community.crypto/issues/568, https://github.com/ansible-collections/community.crypto/pull/784).
v2.20.0
=======
Release Summary
---------------
Feature and bugfix release.
The deprecations in this release are only relevant for collections that use shared
code or docs fragments from this collection.
Minor Changes
-------------
- acme_certificate - add ``include_renewal_cert_id`` option to allow requesting renewal of a specific certificate according to the current ACME Renewal Information specification draft (https://github.com/ansible-collections/community.crypto/pull/739).
Deprecated Features
-------------------
- acme documentation fragment - the default ``community.crypto.acme[.documentation]`` docs fragment is deprecated and will be removed from community.crypto 3.0.0. Replace it with both the new ``community.crypto.acme.basic`` and ``community.crypto.acme.account`` fragments (https://github.com/ansible-collections/community.crypto/pull/735).
- acme.backends module utils - the ``get_cert_information()`` method for a ACME crypto backend must be implemented from community.crypto 3.0.0 on (https://github.com/ansible-collections/community.crypto/pull/736).
- crypto.module_backends.common module utils - the ``crypto.module_backends.common`` module utils is deprecated and will be removed from community.crypto 3.0.0. Use the improved ``argspec`` module util instead (https://github.com/ansible-collections/community.crypto/pull/749).
Bugfixes
--------
- x509_crl, x509_certificate, x509_certificate_info - when parsing absolute timestamps which omitted the second count, the first digit of the minutes was used as a one-digit minutes count, and the second digit of the minutes as a one-digit second count (https://github.com/ansible-collections/community.crypto/pull/745).
New Modules
-----------
- community.crypto.acme_ari_info - Retrieves ACME Renewal Information (ARI) for a certificate.
- community.crypto.acme_certificate_deactivate_authz - Deactivate all authz for an ACME v2 order.
- community.crypto.acme_certificate_renewal_info - Determine whether a certificate should be renewed or not.
v2.19.1
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- crypto.math module utils - change return values for ``quick_is_not_prime()`` and ``convert_int_to_bytes(0, 0)`` for special cases that do not appear when using the collection (https://github.com/ansible-collections/community.crypto/pull/733).
- ecs_certificate - fixed ``csr`` option to be empty and allow renewal of a specific certificate according to the Renewal Information specification (https://github.com/ansible-collections/community.crypto/pull/740).
- x509_certificate - since community.crypto 2.19.0 the module was no longer idempotent with respect to ``not_before`` and ``not_after`` times. This is now fixed (https://github.com/ansible-collections/community.crypto/issues/753, https://github.com/ansible-collections/community.crypto/pull/754).
v2.19.0
=======
Release Summary
---------------
Bugfix and feature release.
Minor Changes
-------------
- When using cryptography >= 42.0.0, use offset-aware ``datetime.datetime`` objects (with timezone UTC) instead of offset-naive UTC timestamps (https://github.com/ansible-collections/community.crypto/issues/726, https://github.com/ansible-collections/community.crypto/pull/727).
- openssh_cert - avoid UTC functions deprecated in Python 3.12 when using Python 3 (https://github.com/ansible-collections/community.crypto/pull/727).
Deprecated Features
-------------------
- acme.backends module utils - from community.crypto on, all implementations of ``CryptoBackend`` must override ``get_ordered_csr_identifiers()``. The current default implementation, which simply sorts the result of ``get_csr_identifiers()``, will then be removed (https://github.com/ansible-collections/community.crypto/pull/725).
Bugfixes
--------
- acme_certificate - respect the order of the CNAME and SAN identifiers that are passed on when creating an ACME order (https://github.com/ansible-collections/community.crypto/issues/723, https://github.com/ansible-collections/community.crypto/pull/725).
New Modules
-----------
- community.crypto.x509_certificate_convert - Convert X.509 certificates
v2.18.0
=======
Release Summary
---------------
Bugfix and feature release.
Minor Changes
-------------
- x509_crl - the new option ``serial_numbers`` allow to configure in which format serial numbers can be provided to ``revoked_certificates[].serial_number``. The default is as integers (``serial_numbers=integer``) for backwards compatibility; setting ``serial_numbers=hex-octets`` allows to specify colon-separated hex octet strings like ``00:11:22:FF`` (https://github.com/ansible-collections/community.crypto/issues/687, https://github.com/ansible-collections/community.crypto/pull/715).
Deprecated Features
-------------------
- openssl_csr_pipe, openssl_privatekey_pipe, x509_certificate_pipe - the current behavior of check mode is deprecated and will change in community.crypto 3.0.0. The current behavior is similar to the modules without ``_pipe``: if the object needs to be (re-)generated, only the ``changed`` status is set, but the object is not updated. From community.crypto 3.0.0 on, the modules will ignore check mode and always act as if check mode is not active. This behavior can already achieved now by adding ``check_mode: false`` to the task. If you think this breaks your use-case of this module, please `create an issue in the community.crypto repository <https://github.com/ansible-collections/community.crypto/issues/new/choose>`__ (https://github.com/ansible-collections/community.crypto/issues/712, https://github.com/ansible-collections/community.crypto/pull/714).
Bugfixes
--------
- luks_device - fixed module a bug that prevented using ``remove_keyslot`` with the value ``0`` (https://github.com/ansible-collections/community.crypto/pull/710).
- luks_device - fixed module falsely outputting ``changed=false`` when trying to add a new slot with a key that is already present in another slot. The module now rejects adding keys that are already present in another slot (https://github.com/ansible-collections/community.crypto/pull/710).
- luks_device - fixed testing of LUKS passphrases in when specifying a keyslot for cryptsetup version 2.0.3. The output of this cryptsetup version slightly differs from later versions (https://github.com/ansible-collections/community.crypto/pull/710).
New Plugins
-----------
Filter
~~~~~~
- community.crypto.parse_serial - Convert a serial number as a colon-separated list of hex numbers to an integer
- community.crypto.to_serial - Convert an integer to a colon-separated list of hex numbers
v2.17.1
=======
Release Summary
---------------
Bugfix release for compatibility with cryptography 42.0.0.
Bugfixes
--------
- openssl_dhparam - was using an internal function instead of the public API to load DH param files when using the ``cryptography`` backend. The internal function was removed in cryptography 42.0.0. The module now uses the public API, which has been available since support for DH params was added to cryptography (https://github.com/ansible-collections/community.crypto/pull/698).
- openssl_privatekey_info - ``check_consistency=true`` no longer works for RSA keys with cryptography 42.0.0+ (https://github.com/ansible-collections/community.crypto/pull/701).
- openssl_privatekey_info - ``check_consistency=true`` now reports a warning if it cannot determine consistency (https://github.com/ansible-collections/community.crypto/pull/705).
v2.17.0
=======
Release Summary
---------------
Feature release.
Minor Changes
-------------
- luks_device - add allow discards option (https://github.com/ansible-collections/community.crypto/pull/693).
v2.16.2
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- acme_* modules - directly react on bad return data for account creation/retrieval/updating requests (https://github.com/ansible-collections/community.crypto/pull/682).
- acme_* modules - fix improved error reporting in case of socket errors, bad status lines, and unknown connection errors (https://github.com/ansible-collections/community.crypto/pull/684).
- acme_* modules - increase number of retries from 5 to 10 to increase stability with unstable ACME endpoints (https://github.com/ansible-collections/community.crypto/pull/685).
- acme_* modules - make account registration handling more flexible to accept 404 instead of 400 send by DigiCert's ACME endpoint when an account does not exist (https://github.com/ansible-collections/community.crypto/pull/681).
v2.16.1
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- acme_* modules - also retry requests in case of socket errors, bad status lines, and unknown connection errors; improve error messages in these cases (https://github.com/ansible-collections/community.crypto/issues/680).
v2.16.0
=======
Release Summary
---------------
Bugfix release.
Minor Changes
-------------
- luks_devices - add new options ``keyslot``, ``new_keyslot``, and ``remove_keyslot`` to allow adding/removing keys to/from specific keyslots (https://github.com/ansible-collections/community.crypto/pull/664).
Bugfixes
--------
- openssl_pkcs12 - modify autodetect to not detect pyOpenSSL >= 23.3.0, which removed PKCS#12 support (https://github.com/ansible-collections/community.crypto/pull/666).
v2.15.1
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- acme_* modules - correctly handle error documents without ``type`` (https://github.com/ansible-collections/community.crypto/issues/651, https://github.com/ansible-collections/community.crypto/pull/652).
v2.15.0
=======
Release Summary
---------------
Bugfix and feature release.
Minor Changes
-------------
- openssh_keypair - fail when comment cannot be updated (https://github.com/ansible-collections/community.crypto/pull/646).
Deprecated Features
-------------------
- get_certificate - the default ``false`` of the ``asn1_base64`` option is deprecated and will change to ``true`` in community.crypto 3.0.0 (https://github.com/ansible-collections/community.crypto/pull/600).
Bugfixes
--------
- openssh_cert, openssh_keypair - the modules ignored return codes of ``ssh`` and ``ssh-keygen`` in some cases (https://github.com/ansible-collections/community.crypto/issues/645, https://github.com/ansible-collections/community.crypto/pull/646).
- openssh_keypair - fix comment updating for OpenSSH before 6.5 (https://github.com/ansible-collections/community.crypto/pull/646).
New Plugins
-----------
Filter
~~~~~~
- community.crypto.gpg_fingerprint - Retrieve a GPG fingerprint from a GPG public or private key
Lookup
~~~~~~
- community.crypto.gpg_fingerprint - Retrieve a GPG fingerprint from a GPG public or private key file
v2.14.1
=======
Release Summary
---------------
Bugfix and maintenance release with updated documentation.
From this version on, community.crypto is using the new `Ansible semantic markup
<https://docs.ansible.com/ansible/devel/dev_guide/developing_modules_documenting.html#semantic-markup-within-module-documentation>`__
in its documentation. If you look at documentation with the ansible-doc CLI tool
from ansible-core before 2.15, please note that it does not render the markup
correctly. You should be still able to read it in most cases, but you need
ansible-core 2.15 or later to see it as it is intended. Alternatively you can
look at `the devel docsite <https://docs.ansible.com/ansible/devel/collections/community/crypto/>`__
for the rendered HTML version of the documentation of the latest release.
Bugfixes
--------
- Fix PEM detection/identification to also accept random other lines before the line starting with ``-----BEGIN`` (https://github.com/ansible-collections/community.crypto/issues/627, https://github.com/ansible-collections/community.crypto/pull/628).
Known Issues
------------
- Ansible markup will show up in raw form on ansible-doc text output for ansible-core before 2.15. If you have trouble deciphering the documentation markup, please upgrade to ansible-core 2.15 (or newer), or read the HTML documentation on https://docs.ansible.com/ansible/devel/collections/community/crypto/.
v2.14.0
=======
Release Summary
---------------
Feature release.
Minor Changes
-------------
- acme_certificate - allow to use no challenge by providing ``no challenge`` for the ``challenge`` option. This is needed for ACME servers where validation is done without challenges (https://github.com/ansible-collections/community.crypto/issues/613, https://github.com/ansible-collections/community.crypto/pull/615).
- acme_certificate - validate and wait for challenges in parallel instead handling them one after another (https://github.com/ansible-collections/community.crypto/pull/617).
- x509_certificate_info - added support for certificates in DER format when using ``path`` parameter (https://github.com/ansible-collections/community.crypto/issues/603).
v2.13.1
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- execution environment definition - fix installation of ``python3-pyOpenSSL`` package on CentOS and RHEL (https://github.com/ansible-collections/community.crypto/pull/606).
- execution environment definition - fix source of ``python3-pyOpenSSL`` package for Rocky Linux 9+ (https://github.com/ansible-collections/community.crypto/pull/606).
v2.13.0
=======
Release Summary
---------------
Bugfix and maintenance release.
Minor Changes
-------------
- x509_crl - the ``crl_mode`` option has been added to replace the existing ``mode`` option (https://github.com/ansible-collections/community.crypto/issues/596).
Deprecated Features
-------------------
- x509_crl - the ``mode`` option is deprecated; use ``crl_mode`` instead. The ``mode`` option will change its meaning in community.crypto 3.0.0, and will refer to the CRL file's mode instead (https://github.com/ansible-collections/community.crypto/issues/596).
Bugfixes
--------
- openssh_keypair - always generate a new key pair if the private key does not exist. Previously, the module would fail when ``regenerate=fail`` without an existing key, contradicting the documentation (https://github.com/ansible-collections/community.crypto/pull/598).
- x509_crl - remove problem with ansible-core 2.16 due to ``AnsibleModule`` is now validating the ``mode`` parameter's values (https://github.com/ansible-collections/community.crypto/issues/596).
v2.12.0 v2.12.0
======= =======
@@ -66,12 +624,12 @@ New Plugins
Filter Filter
~~~~~~ ~~~~~~
- openssl_csr_info - Retrieve information from OpenSSL Certificate Signing Requests (CSR) - community.crypto.openssl_csr_info - Retrieve information from OpenSSL Certificate Signing Requests (CSR)
- openssl_privatekey_info - Retrieve information from OpenSSL private keys - community.crypto.openssl_privatekey_info - Retrieve information from OpenSSL private keys
- openssl_publickey_info - Retrieve information from OpenSSL public keys in PEM format - community.crypto.openssl_publickey_info - Retrieve information from OpenSSL public keys in PEM format
- split_pem - Split PEM file contents into multiple objects - community.crypto.split_pem - Split PEM file contents into multiple objects
- x509_certificate_info - Retrieve information from X.509 certificates in PEM format - community.crypto.x509_certificate_info - Retrieve information from X.509 certificates in PEM format
- x509_crl_info - Retrieve information from X.509 CRLs in PEM format - community.crypto.x509_crl_info - Retrieve information from X.509 CRLs in PEM format
v2.9.0 v2.9.0
====== ======
@@ -201,7 +759,6 @@ This release is identical to what should have been 2.3.3, except that the
version number has been bumped to 2.3.4 and this changelog entry for 2.3.4 version number has been bumped to 2.3.4 and this changelog entry for 2.3.4
has been added. has been added.
v2.3.3 v2.3.3
====== ======
@@ -256,7 +813,7 @@ Minor Changes
------------- -------------
- Prepare collection for inclusion in an Execution Environment by declaring its dependencies. Please note that system packages are used for cryptography and PyOpenSSL, which can be rather limited. If you need features from newer cryptography versions, you will have to manually force a newer version to be installed by pip by specifying something like ``cryptography >= 37.0.0`` in your Execution Environment's Python dependencies file (https://github.com/ansible-collections/community.crypto/pull/440). - Prepare collection for inclusion in an Execution Environment by declaring its dependencies. Please note that system packages are used for cryptography and PyOpenSSL, which can be rather limited. If you need features from newer cryptography versions, you will have to manually force a newer version to be installed by pip by specifying something like ``cryptography >= 37.0.0`` in your Execution Environment's Python dependencies file (https://github.com/ansible-collections/community.crypto/pull/440).
- Support automatic conversion for Internalionalized Domain Names (IDNs). When passing general names, for example Subject Altenative Names to ``community.crypto.openssl_csr``, these will automatically be converted to IDNA. Conversion will be done per label to IDNA2008 if possible, and IDNA2003 if IDNA2008 conversion fails for that label. Note that IDNA conversion requires `the Python idna library <https://pypi.org/project/idna/>`_ to be installed. Please note that depending on which versions of the cryptography library are used, it could try to process the converted IDNA another time with the Python ``idna`` library and reject IDNA2003 encoded values. Using a new enough ``cryptography`` version avoids this (https://github.com/ansible-collections/community.crypto/issues/426, https://github.com/ansible-collections/community.crypto/pull/436). - Support automatic conversion for Internalionalized Domain Names (IDNs). When passing general names, for example Subject Alternative Names to ``community.crypto.openssl_csr``, these will automatically be converted to IDNA. Conversion will be done per label to IDNA2008 if possible, and IDNA2003 if IDNA2008 conversion fails for that label. Note that IDNA conversion requires `the Python idna library <https://pypi.org/project/idna/>`_ to be installed. Please note that depending on which versions of the cryptography library are used, it could try to process the converted IDNA another time with the Python ``idna`` library and reject IDNA2003 encoded values. Using a new enough ``cryptography`` version avoids this (https://github.com/ansible-collections/community.crypto/issues/426, https://github.com/ansible-collections/community.crypto/pull/436).
- acme_* modules - add parameter ``request_timeout`` to manage HTTP(S) request timeout (https://github.com/ansible-collections/community.crypto/issues/447, https://github.com/ansible-collections/community.crypto/pull/448). - acme_* modules - add parameter ``request_timeout`` to manage HTTP(S) request timeout (https://github.com/ansible-collections/community.crypto/issues/447, https://github.com/ansible-collections/community.crypto/pull/448).
- luks_devices - added ``perf_same_cpu_crypt``, ``perf_submit_from_crypt_cpus``, ``perf_no_read_workqueue``, ``perf_no_write_workqueue`` for performance tuning when opening LUKS2 containers (https://github.com/ansible-collections/community.crypto/issues/427). - luks_devices - added ``perf_same_cpu_crypt``, ``perf_submit_from_crypt_cpus``, ``perf_no_read_workqueue``, ``perf_no_write_workqueue`` for performance tuning when opening LUKS2 containers (https://github.com/ansible-collections/community.crypto/issues/427).
- luks_devices - added ``persistent`` option when opening LUKS2 containers (https://github.com/ansible-collections/community.crypto/pull/434). - luks_devices - added ``persistent`` option when opening LUKS2 containers (https://github.com/ansible-collections/community.crypto/pull/434).
@@ -308,7 +865,6 @@ Regular bugfix release.
In this release, we extended the test matrix to include Alpine 3, ArchLinux, Debian Bullseye, and CentOS Stream 8. CentOS 8 was removed from the test matrix. In this release, we extended the test matrix to include Alpine 3, ArchLinux, Debian Bullseye, and CentOS Stream 8. CentOS 8 was removed from the test matrix.
Bugfixes Bugfixes
-------- --------
@@ -372,8 +928,8 @@ Bugfixes
New Modules New Modules
----------- -----------
- crypto_info - Retrieve cryptographic capabilities - community.crypto.crypto_info - Retrieve cryptographic capabilities
- openssl_privatekey_convert - Convert OpenSSL private keys - community.crypto.openssl_privatekey_convert - Convert OpenSSL private keys
v2.0.2 v2.0.2
====== ======
@@ -412,7 +968,6 @@ Release Summary
A new major release of the ``community.crypto`` collection. The main changes are removal of the PyOpenSSL backends for almost all modules (``openssl_pkcs12`` being the only exception), and removal of the ``assertonly`` provider in the ``x509_certificate`` provider. There are also some other breaking changes which should improve the user interface/experience of this collection long-term. A new major release of the ``community.crypto`` collection. The main changes are removal of the PyOpenSSL backends for almost all modules (``openssl_pkcs12`` being the only exception), and removal of the ``assertonly`` provider in the ``x509_certificate`` provider. There are also some other breaking changes which should improve the user interface/experience of this collection long-term.
Minor Changes Minor Changes
------------- -------------
@@ -595,20 +1150,20 @@ Minor Changes
- openssh_keypair - added ``passphrase`` parameter for encrypting/decrypting OpenSSH private keys (https://github.com/ansible-collections/community.crypto/pull/225). - openssh_keypair - added ``passphrase`` parameter for encrypting/decrypting OpenSSH private keys (https://github.com/ansible-collections/community.crypto/pull/225).
- openssl_csr - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). - openssl_csr - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- openssl_csr_info - now returns ``public_key_type`` and ``public_key_data`` (https://github.com/ansible-collections/community.crypto/pull/233). - openssl_csr_info - now returns ``public_key_type`` and ``public_key_data`` (https://github.com/ansible-collections/community.crypto/pull/233).
- openssl_csr_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/204). - openssl_csr_info - refactor module to allow code reuse for diff mode (https://github.com/ansible-collections/community.crypto/pull/204).
- openssl_csr_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). - openssl_csr_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- openssl_pkcs12 - added option ``select_crypto_backend`` and a ``cryptography`` backend. This requires cryptography 3.0 or newer, and does not support the ``iter_size`` and ``maciter_size`` options (https://github.com/ansible-collections/community.crypto/pull/234). - openssl_pkcs12 - added option ``select_crypto_backend`` and a ``cryptography`` backend. This requires cryptography 3.0 or newer, and does not support the ``iter_size`` and ``maciter_size`` options (https://github.com/ansible-collections/community.crypto/pull/234).
- openssl_privatekey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). - openssl_privatekey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- openssl_privatekey_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/205). - openssl_privatekey_info - refactor module to allow code reuse for diff mode (https://github.com/ansible-collections/community.crypto/pull/205).
- openssl_privatekey_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). - openssl_privatekey_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- openssl_publickey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). - openssl_publickey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- x509_certificate - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). - x509_certificate - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- x509_certificate_info - now returns ``public_key_type`` and ``public_key_data`` (https://github.com/ansible-collections/community.crypto/pull/233). - x509_certificate_info - now returns ``public_key_type`` and ``public_key_data`` (https://github.com/ansible-collections/community.crypto/pull/233).
- x509_certificate_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/206). - x509_certificate_info - refactor module to allow code reuse for diff mode (https://github.com/ansible-collections/community.crypto/pull/206).
- x509_certificate_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). - x509_certificate_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- x509_crl - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150). - x509_crl - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- x509_crl_info - add ``list_revoked_certificates`` option to avoid enumerating all revoked certificates (https://github.com/ansible-collections/community.crypto/pull/232). - x509_crl_info - add ``list_revoked_certificates`` option to avoid enumerating all revoked certificates (https://github.com/ansible-collections/community.crypto/pull/232).
- x509_crl_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/203). - x509_crl_info - refactor module to allow code reuse for diff mode (https://github.com/ansible-collections/community.crypto/pull/203).
Bugfixes Bugfixes
-------- --------
@@ -620,7 +1175,7 @@ Bugfixes
New Modules New Modules
----------- -----------
- openssl_publickey_info - Provide information for OpenSSL public keys - community.crypto.openssl_publickey_info - Provide information for OpenSSL public keys
v1.6.2 v1.6.2
====== ======
@@ -731,16 +1286,15 @@ Release Summary
Contains new modules ``openssl_privatekey_pipe``, ``openssl_csr_pipe`` and ``x509_certificate_pipe`` which allow to create or update private keys, CSRs and X.509 certificates without having to write them to disk. Contains new modules ``openssl_privatekey_pipe``, ``openssl_csr_pipe`` and ``x509_certificate_pipe`` which allow to create or update private keys, CSRs and X.509 certificates without having to write them to disk.
Minor Changes Minor Changes
------------- -------------
- openssh_cert - add module parameter ``use_agent`` to enable using signing keys stored in ssh-agent (https://github.com/ansible-collections/community.crypto/issues/116). - openssh_cert - add module parameter ``use_agent`` to enable using signing keys stored in ssh-agent (https://github.com/ansible-collections/community.crypto/issues/116).
- openssl_csr - refactor module to allow code re-use by openssl_csr_pipe (https://github.com/ansible-collections/community.crypto/pull/123). - openssl_csr - refactor module to allow code reuse by openssl_csr_pipe (https://github.com/ansible-collections/community.crypto/pull/123).
- openssl_privatekey - refactor module to allow code re-use by openssl_privatekey_pipe (https://github.com/ansible-collections/community.crypto/pull/119). - openssl_privatekey - refactor module to allow code reuse by openssl_privatekey_pipe (https://github.com/ansible-collections/community.crypto/pull/119).
- openssl_privatekey - the elliptic curve ``secp192r1`` now triggers a security warning. Elliptic curves of at least 224 bits should be used for new keys; see `here <https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec.html#elliptic-curves>`_ (https://github.com/ansible-collections/community.crypto/pull/132). - openssl_privatekey - the elliptic curve ``secp192r1`` now triggers a security warning. Elliptic curves of at least 224 bits should be used for new keys; see `here <https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec.html#elliptic-curves>`_ (https://github.com/ansible-collections/community.crypto/pull/132).
- x509_certificate - for the ``selfsigned`` provider, a CSR is not required anymore. If no CSR is provided, the module behaves as if a minimal CSR which only contains the public key has been provided (https://github.com/ansible-collections/community.crypto/issues/32, https://github.com/ansible-collections/community.crypto/pull/129). - x509_certificate - for the ``selfsigned`` provider, a CSR is not required anymore. If no CSR is provided, the module behaves as if a minimal CSR which only contains the public key has been provided (https://github.com/ansible-collections/community.crypto/issues/32, https://github.com/ansible-collections/community.crypto/pull/129).
- x509_certificate - refactor module to allow code re-use by x509_certificate_pipe (https://github.com/ansible-collections/community.crypto/pull/135). - x509_certificate - refactor module to allow code reuse by x509_certificate_pipe (https://github.com/ansible-collections/community.crypto/pull/135).
Bugfixes Bugfixes
-------- --------
@@ -752,9 +1306,9 @@ Bugfixes
New Modules New Modules
----------- -----------
- openssl_csr_pipe - Generate OpenSSL Certificate Signing Request (CSR) - community.crypto.openssl_csr_pipe - Generate OpenSSL Certificate Signing Request (CSR)
- openssl_privatekey_pipe - Generate OpenSSL private keys without disk access - community.crypto.openssl_privatekey_pipe - Generate OpenSSL private keys without disk access
- x509_certificate_pipe - Generate and/or check OpenSSL certificates - community.crypto.x509_certificate_pipe - Generate and/or check OpenSSL certificates
v1.2.0 v1.2.0
====== ======
@@ -807,7 +1361,6 @@ Release Summary
Release for Ansible 2.10.0. Release for Ansible 2.10.0.
Minor Changes Minor Changes
------------- -------------
@@ -831,8 +1384,8 @@ Bugfixes
New Modules New Modules
----------- -----------
- openssl_signature - Sign data with openssl - community.crypto.openssl_signature - Sign data with openssl
- openssl_signature_info - Verify signatures with openssl - community.crypto.openssl_signature_info - Verify signatures with openssl
v1.0.0 v1.0.0
====== ======
@@ -842,7 +1395,6 @@ Release Summary
This is the first proper release of the ``community.crypto`` collection. This changelog contains all changes to the modules in this collection that were added after the release of Ansible 2.9.0. This is the first proper release of the ``community.crypto`` collection. This changelog contains all changes to the modules in this collection that were added after the release of Ansible 2.9.0.
Minor Changes Minor Changes
------------- -------------
@@ -853,7 +1405,7 @@ Minor Changes
- openssh_keypair - instead of regenerating some broken or password protected keys, fail the module. Keys can still be regenerated by calling the module with ``force=yes``. - openssh_keypair - instead of regenerating some broken or password protected keys, fail the module. Keys can still be regenerated by calling the module with ``force=yes``.
- openssh_keypair - the ``regenerate`` option allows to configure the module's behavior when it should or needs to regenerate private keys. - openssh_keypair - the ``regenerate`` option allows to configure the module's behavior when it should or needs to regenerate private keys.
- openssl_* modules - the cryptography backend now properly supports ``dirName``, ``otherName`` and ``RID`` (Registered ID) names. - openssl_* modules - the cryptography backend now properly supports ``dirName``, ``otherName`` and ``RID`` (Registered ID) names.
- openssl_certificate - Add option for changing which ACME directory to use with acme-tiny. Set the default ACME directory to Let's Encrypt instead of using acme-tiny's default. (acme-tiny also uses Let's Encrypt at the time being, so no action should be neccessary.) - openssl_certificate - Add option for changing which ACME directory to use with acme-tiny. Set the default ACME directory to Let's Encrypt instead of using acme-tiny's default. (acme-tiny also uses Let's Encrypt at the time being, so no action should be necessary.)
- openssl_certificate - Change the required version of acme-tiny to >= 4.0.0 - openssl_certificate - Change the required version of acme-tiny to >= 4.0.0
- openssl_certificate - allow to provide content of some input files via the ``csr_content``, ``privatekey_content``, ``ownca_privatekey_content`` and ``ownca_content`` options. - openssl_certificate - allow to provide content of some input files via the ``csr_content``, ``privatekey_content``, ``ownca_privatekey_content`` and ``ownca_content`` options.
- openssl_certificate - allow to return the existing/generated certificate directly as ``certificate`` by setting ``return_content`` to ``yes``. - openssl_certificate - allow to return the existing/generated certificate directly as ``certificate`` by setting ``return_content`` to ``yes``.
@@ -908,6 +1460,6 @@ Bugfixes
New Modules New Modules
----------- -----------
- ecs_domain - Request validation of a domain with the Entrust Certificate Services (ECS) API - community.crypto.ecs_domain - Request validation of a domain with the Entrust Certificate Services (ECS) API
- x509_crl - Generate Certificate Revocation Lists (CRLs) - community.crypto.x509_crl - Generate Certificate Revocation Lists (CRLs)
- x509_crl_info - Retrieve information on Certificate Revocation Lists (CRLs) - community.crypto.x509_crl_info - Retrieve information on Certificate Revocation Lists (CRLs)

View File

@@ -1,48 +0,0 @@
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation;
All Rights Reserved" are retained in Python alone or in any derivative version
prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.

View File

@@ -6,9 +6,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/)
[![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=main)](https://dev.azure.com/ansible/community.crypto/_build?definitionId=21)
[![EOL CI](https://github.com/ansible-collections/community.crypto/workflows/EOL%20CI/badge.svg?event=push)](https://github.com/ansible-collections/community.crypto/actions) [![Nox CI](https://github.com/ansible-collections/community.crypto/actions/workflows/nox.yml/badge.svg?branch=main)](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.
@@ -16,15 +18,34 @@ You can find [documentation for this collection on the Ansible docs site](https:
Please note that this collection does **not** support Windows targets. Please note that this collection does **not** support Windows targets.
## Code of Conduct
We follow [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) in all our interactions within this project.
If you encounter abusive behavior violating the [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html), please refer to the [policy violations](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html#policy-violations) section of the Code of Conduct for information on how to raise a complaint.
## Communication
* Join the Ansible forum:
* [Get Help](https://forum.ansible.com/c/help/6): get help or help others. Please add appropriate tags if you start new discussions, for example the `crypto` or `acme` tags.
* [Posts tagged with 'crypto'](https://forum.ansible.com/tag/crypto): subscribe to participate in cryptography related conversations.
* [Posts tagged with 'acme'](https://forum.ansible.com/tag/acme): subscribe to participate in ACME (RFC 8555) related conversations.
* [Social Spaces](https://forum.ansible.com/c/chat/4): gather and interact with fellow enthusiasts.
* [News & Announcements](https://forum.ansible.com/c/news/5): track project-wide announcements including social events.
* The Ansible [Bullhorn newsletter](https://docs.ansible.com/ansible/devel/community/communication.html#the-bullhorn): used to announce releases and important changes.
For more information about communication, see the [Ansible communication guide](https://docs.ansible.com/ansible/devel/community/communication.html).
## 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, and ansible-core 2.14 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported. Tested with the current ansible-core-2.17, ansible-core 2.18, and ansible-core 2.19 releases and the current development version of ansible-core. Ansible-core versions before 2.17 are not supported; please use community.crypto 2.x.y with these.
## External requirements ## External requirements
The exact requirements for every module are listed in the module documentation. The exact requirements for every module are listed in the module documentation.
Most modules require a recent enough version of [the Python cryptography library](https://pypi.org/project/cryptography/). See the module documentations for the minimal version supported for each module. Most modules require a recent enough version of [the Python cryptography library](https://pypi.org/project/cryptography/); the minimum supported version by this collection is 3.3. See the module documentations for the minimal version supported for each module.
## Collection Documentation ## Collection Documentation
@@ -36,42 +57,6 @@ We also separately publish [**latest commit** collection documentation](https://
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**.
## Included content
- OpenSSL / PKI modules:
- openssl_csr_info
- openssl_csr
- openssl_dhparam
- openssl_pkcs12
- openssl_privatekey_info
- openssl_privatekey
- openssl_publickey
- openssl_signature_info
- openssl_signature
- x509_certificate_info
- x509_certificate
- x509_crl_info
- x509_crl
- certificate_complete_chain
- OpenSSH modules:
- openssh_cert
- openssh_keypair
- ACME modules:
- acme_account_info
- acme_account
- acme_certificate
- acme_certificate_revoke
- acme_challenge_cert_helper
- acme_inspect
- ECS modules:
- ecs_certificate
- ecs_domain
- Miscellaneous modules:
- get_certificate
- luks_device
You can also find a list of all modules with documentation on the [Ansible docs site](https://docs.ansible.com/ansible/latest/collections/community/crypto/).
## Using this collection ## Using this collection
Before using the crypto community collection, you need to install the collection with the `ansible-galaxy` CLI: Before using the crypto community collection, you need to install the collection with the `ansible-galaxy` CLI:
@@ -102,20 +87,12 @@ 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.rst). See the [changelog](https://github.com/ansible-collections/community.crypto/blob/main/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``.
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.
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
- [Ansible Collection overview](https://github.com/ansible-collections/overview) - [Ansible Collection overview](https://github.com/ansible-collections/overview)
@@ -129,6 +106,6 @@ This collection is primarily licensed and distributed as a whole under the GNU G
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/main/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`), 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/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`). 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"

57
antsibull-nox.toml Normal file
View File

@@ -0,0 +1,57 @@
# 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 = ".isort.cfg"
run_black = true
run_flake8 = true
flake8_config = ".flake8"
run_pylint = true
pylint_rcfile = ".pylintrc"
pylint_ansible_core_package = "ansible-core>=2.19.0b4"
run_yamllint = true
yamllint_config = ".yamllint"
yamllint_config_plugins = ".yamllint-docs"
yamllint_config_plugins_examples = ".yamllint-examples"
run_mypy = true
mypy_ansible_core_package = "ansible-core>=2.19.0b4"
mypy_config = ".mypy.ini"
mypy_extra_deps = [
"cryptography",
"types-mock",
"types-PyYAML",
]
[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]

File diff suppressed because it is too large Load Diff

View File

@@ -11,23 +11,31 @@ keep_fragments: false
mention_ancestor: true mention_ancestor: true
new_plugins_after_name: removed_features new_plugins_after_name: removed_features
notesdir: fragments notesdir: fragments
output_formats:
- md
- rst
prelude_section_name: release_summary prelude_section_name: release_summary
prelude_section_title: Release Summary prelude_section_title: Release Summary
sections: sections:
- - major_changes - - major_changes
- Major Changes - Major Changes
- - minor_changes - - minor_changes
- Minor Changes - Minor Changes
- - breaking_changes - - breaking_changes
- Breaking Changes / Porting Guide - Breaking Changes / Porting Guide
- - deprecated_features - - deprecated_features
- Deprecated Features - Deprecated Features
- - removed_features - - removed_features
- Removed Features (previously deprecated) - Removed Features (previously deprecated)
- - security_fixes - - security_fixes
- Security Fixes - Security Fixes
- - bugfixes - - bugfixes
- Bugfixes - Bugfixes
- - known_issues - - known_issues
- Known Issues - Known Issues
title: Community Crypto title: Community Crypto
trivial_section_name: trivial
use_fqcn: true
add_plugin_period: true
changelog_nice_yaml: true
changelog_sort: version

View File

@@ -3,5 +3,5 @@
# 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
pyopenssl_package_name: py-openssl changelog:
pyopenssl_package_name_python3: py3-openssl write_changelog: true

View File

@@ -9,6 +9,10 @@ edit_on_github:
path_prefix: '' path_prefix: ''
extra_links: extra_links:
- description: Ask for help (crypto)
url: https://forum.ansible.com/tags/c/help/6/none/crypto
- description: Ask for help (ACME)
url: https://forum.ansible.com/tags/c/help/6/none/acme
- description: Submit a bug report - description: Submit a bug report
url: https://github.com/ansible-collections/community.crypto/issues/new?assignees=&labels=&template=bug_report.md url: https://github.com/ansible-collections/community.crypto/issues/new?assignees=&labels=&template=bug_report.md
- description: Request a feature - description: Request a feature
@@ -22,6 +26,13 @@ communication:
- topic: General usage and support questions - topic: General usage and support questions
network: Libera network: Libera
channel: '#ansible' channel: '#ansible'
mailing_lists: forums:
- topic: Ansible Project List - topic: "Ansible Forum: General usage and support questions"
url: https://groups.google.com/g/ansible-project # The following URL directly points to the "Get Help" section
url: https://forum.ansible.com/c/help/6/none
- topic: "Ansible Forum: Discussions about cryptography"
# The following URL directly points to the "crpyto" tag
url: https://forum.ansible.com/tag/crpyto
- topic: "Ansible Forum: Discussions about ACME (RFC 8555)"
# The following URL directly points to the "acme" tag
url: https://forum.ansible.com/tag/acme

View File

@@ -8,7 +8,7 @@
How to create a small CA How to create a small CA
======================== ========================
The `community.crypto collection <https://galaxy.ansible.com/community/crypto>`_ offers multiple modules that create private keys, certificate signing requests, and certificates. This guide shows how to create your own small CA and how to use it to sign certificates. 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 your own small CA and how to use it to sign certificates.
In all examples, we assume that the CA's private key is password protected, where the password is provided in the ``secret_ca_passphrase`` variable. In all examples, we assume that the CA's private key is password protected, where the password is provided in the ``secret_ca_passphrase`` variable.

View File

@@ -8,9 +8,9 @@
How to create self-signed certificates How to create self-signed certificates
====================================== ======================================
The `community.crypto collection <https://galaxy.ansible.com/community/crypto>`_ offers multiple modules that create private keys, certificate signing requests, and certificates. This guide shows how to create self-signed certificates. 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 ``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 :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:
.. code-block:: yaml+jinja .. code-block:: yaml+jinja
@@ -18,7 +18,7 @@ For creating any kind of certificate, you always have to start with a private ke
community.crypto.openssl_privatekey: community.crypto.openssl_privatekey:
path: /path/to/certificate.key path: /path/to/certificate.key
You can specify ``type`` to select another key type, ``size`` to select a different key size (only available for RSA and DSA keys), or ``passphrase`` if you want to store the key password-protected: You can specify :ansopt:`community.crypto.openssl_privatekey#module:type` to select another key type, :ansopt:`community.crypto.openssl_privatekey#module:size` to select a different key size (only available for RSA and DSA keys), or :ansopt:`community.crypto.openssl_privatekey#module:passphrase` if you want to store the key password-protected:
.. code-block:: yaml+jinja .. code-block:: yaml+jinja
@@ -38,9 +38,9 @@ To create a very simple self-signed certificate with no specific information, yo
privatekey_path: /path/to/certificate.key privatekey_path: /path/to/certificate.key
provider: selfsigned provider: selfsigned
(If you used ``passphrase`` for the private key, you have to provide ``privatekey_passphrase``.) (If you used :ansopt:`community.crypto.openssl_privatekey#module:passphrase` for the private key, you have to provide :ansopt:`community.crypto.x509_certificate#module:privatekey_passphrase`.)
You can use ``selfsigned_not_after`` to define when the certificate expires (default: in roughly 10 years), and ``selfsigned_not_before`` to define from when the certificate is valid (default: now). 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 :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.)

View File

@@ -5,18 +5,18 @@
namespace: community namespace: community
name: crypto name: crypto
version: 2.12.0 version: 3.0.0-a1
readme: README.md readme: README.md
authors: authors:
- Ansible (github.com/ansible) - Ansible (github.com/ansible)
description: null description: Provides modules and plugins for many cryptographic operations.
license: license:
- GPL-3.0-or-later - GPL-3.0-or-later
- Apache-2.0 - Apache-2.0
- 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

View File

@@ -11,7 +11,3 @@ openssl [platform:rpm]
python3-cryptography [platform:dpkg] python3-cryptography [platform:dpkg]
python3-cryptography [platform:rpm] python3-cryptography [platform:rpm]
python3-openssl [platform:dpkg] python3-openssl [platform:dpkg]
# On RHEL 9+ and CentOS Stream 9+, python3-pyOpenSSL is part of EPEL
python3-pyOpenSSL [platform:rpm !platform:rhel !platform:centos]
python3-pyOpenSSL [platform:rhel-6 platform:centos-6 platform:rhel-7 platform:centos-7 platform:rhel-8 platform:centos-8]
python3-pyOpenSSL [platform:rhel platform:centos !platform:rhel-6 !platform:centos-6 !platform:rhel-7 !platform:centos-7 !platform:rhel-8 !platform:centos-8 epel]

View File

@@ -3,13 +3,18 @@
# 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
requires_ansible: '>=2.9.10' requires_ansible: '>=2.17.0'
action_groups: action_groups:
acme: acme:
- acme_inspect - acme_inspect
- acme_certificate_revoke
- acme_certificate - acme_certificate
- acme_certificate_deactivate_authz
- acme_certificate_order_create
- acme_certificate_order_finalize
- acme_certificate_order_info
- acme_certificate_order_validate
- acme_certificate_revoke
- acme_account - acme_account
- acme_account_info - acme_account_info

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

@@ -1,91 +1,100 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Felix Fontein <felix@fontein.de> # Copyright (c) 2020, Felix Fontein <felix@fontein.de>
# 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 annotations
__metaclass__ = type
import base64 import base64
import typing as t
from ansible.module_utils.common.text.converters import to_native, to_bytes from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
from ansible_collections.community.crypto.plugins.plugin_utils.action_module import ActionModuleBase
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,
) )
class PrivateKeyModule(object): if t.TYPE_CHECKING:
def __init__(self, module, module_backend): from ansible_collections.community.crypto.plugins.module_utils._argspec import (
ArgumentSpec,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.privatekey import (
PrivateKeyBackend,
)
from ansible_collections.community.crypto.plugins.plugin_utils._action_module import (
AnsibleActionModule,
)
class PrivateKeyModule:
def __init__(
self, module: AnsibleActionModule, module_backend: PrivateKeyBackend
) -> None:
self.module = module self.module = module
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: bool = module.params["return_current_key"]
if module.params['content'] is not None: content: str | None = module.params["content"]
if module.params['content_base64']: content_base64: bool = module.params["content_base64"]
if content is not None:
if content_base64:
try: try:
data = base64.b64decode(module.params['content']) data = base64.b64decode(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=f"Cannot decode Base64 encoded data: {e}")
else: else:
data = to_bytes(module.params['content']) data = to_bytes(content)
module_backend.set_existing(data) module_backend.set_existing(privatekey_bytes=data)
def generate(self, module): def generate(self, module: AnsibleActionModule) -> None:
"""Generate a keypair.""" """Generate a keypair."""
if self.module_backend.needs_regeneration(): if self.module_backend.needs_regeneration():
# Regenerate # Regenerate
if not self.check_mode: self.module_backend.generate_private_key()
self.module_backend.generate_private_key() # Call get_private_key_data() to make sure that exceptions are raised now:
privatekey_data = self.module_backend.get_private_key_data() self.module_backend.get_private_key_data()
self.privatekey_bytes = privatekey_data
self.changed = True self.changed = True
elif self.module_backend.needs_conversion(): elif self.module_backend.needs_conversion():
# Convert # Convert
if not self.check_mode: self.module_backend.convert_private_key()
self.module_backend.convert_private_key() # Call get_private_key_data() to make sure that exceptions are raised now:
privatekey_data = self.module_backend.get_private_key_data() self.module_backend.get_private_key_data()
self.privatekey_bytes = privatekey_data
self.changed = True self.changed = True
def dump(self): def dump(self) -> dict[str, t.Any]:
"""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
class ActionModule(ActionModuleBase): class ActionModule(ActionModuleBase):
@staticmethod def setup_module(self) -> tuple[ArgumentSpec, dict[str, t.Any]]:
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), {
content_base64=dict(type='bool', default=False), "content": {"type": "str", "no_log": True},
return_current_key=dict(type='bool', default=False), "content_base64": {"type": "bool", "default": False},
)) "return_current_key": {"type": "bool", "default": False},
return argument_spec, dict( }
supports_check_mode=True,
) )
return argument_spec, {
"supports_check_mode": True,
}
@staticmethod def run_module(self, module: AnsibleActionModule) -> None:
def run_module(module): module_backend = select_backend(module=module)
backend, module_backend = select_backend(
module=module,
backend=module.params['select_crypto_backend'],
)
try: try:
private_key = PrivateKeyModule(module, module_backend) private_key = PrivateKeyModule(module, module_backend)
@@ -99,10 +108,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=str(exc))

View File

@@ -0,0 +1,152 @@
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# 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
# Note that this doc fragment is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
class ModuleDocFragment:
# Basic documentation fragment without account data
BASIC = r"""
notes:
- Although the defaults are chosen so that the module can be used with the L(Let's Encrypt,https://letsencrypt.org/) CA,
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
(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
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.
requirements:
- either C(openssl)
- or L(cryptography,https://cryptography.io/) >= 3.3
options:
acme_version:
description:
- The ACME version of the endpoint.
- Must be V(2) for standardized ACME v2 endpoints.
- The value V(1) is no longer supported since community.crypto 3.0.0.
type: int
default: 2
choices:
- 2
acme_directory:
description:
- The ACME directory to use. This is the entry point URL to access the ACME CA server API.
- For safety reasons the default is set to the Let's Encrypt staging server (for the ACME v1 protocol). This will create
technically correct, but untrusted certificates.
- "For Let's Encrypt, all staging endpoints can be found here: U(https://letsencrypt.org/docs/staging-environment/).
For Buypass, all endpoints can be found here: U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)."
- For B(Let's Encrypt), the production directory URL for ACME v2 is U(https://acme-v02.api.letsencrypt.org/directory).
- 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(Sectigo), the production directory URL for ACME v2 is U(https://acme-qa.secure.trust-provider.com/v2/DV).
- The notes for this module contain a list of ACME services this module has been tested against.
required: true
type: str
validate_certs:
description:
- Whether calls to the ACME directory will validate TLS certificates.
- B(Warning:) Should B(only ever) be set to V(false) for testing purposes, for example when testing against a local
Pebble server.
type: bool
default: true
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is V(auto), which tries to use C(cryptography) if available, and falls back to C(openssl).
- If set to V(openssl), will try to use the C(openssl) binary.
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [auto, cryptography, openssl]
request_timeout:
description:
- The time Ansible should wait for a response from the ACME API.
- This timeout is applied to all HTTP(S) requests (HEAD, GET, POST).
type: int
default: 10
version_added: 2.3.0
"""
# Account data documentation fragment
ACCOUNT = r"""
notes:
- If a new enough version of the C(cryptography) library is available (see Requirements for details), it will be used instead
of the C(openssl) binary. This can be explicitly disabled or enabled with the O(select_crypto_backend) option. Note that
using the C(openssl) binary will be slower and less secure, as private key contents always have to be stored on disk (see
O(account_key_content)).
options:
account_key_src:
description:
- Path to a file containing the ACME account RSA or Elliptic Curve key.
- '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
line tool: RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys can be created with C(openssl ecparam
-genkey ...). Any other tool creating private keys in PEM format can be used as well.'
- Mutually exclusive with O(account_key_content).
- Required if O(account_key_content) is not used.
type: path
aliases:
- account_key
account_key_content:
description:
- Content of the ACME account RSA or Elliptic Curve key.
- Mutually exclusive with O(account_key_src).
- 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.
Since this is an important private key — it can be used to change the account key, or to revoke your certificates
without knowing their private keys —, this might not be acceptable.
- In case C(cryptography) is used, the content is not written into a temporary file. It can still happen that it is
written to disk by Ansible in the process of moving the module with its argument to the node where it is executed.
type: str
account_key_passphrase:
description:
- Phassphrase to use to decode the account key.
- B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend.
type: str
version_added: 1.6.0
account_uri:
description:
- If specified, assumes that the account URI is as given. If the account key does not match this account, or an account
with this URI does not exist, the module fails.
type: str
"""
# No account data documentation fragment
NO_ACCOUNT = r"""
notes:
- "If a new enough version of the C(cryptography) library
is available (see Requirements for details), it will be used
instead of the C(openssl) binary. This can be explicitly disabled
or enabled with the O(select_crypto_backend) option. Note that using
the C(openssl) binary will be slower."
options: {}
"""
CERTIFICATE = r"""
options:
csr:
description:
- File containing the CSR for the new certificate.
- Can be created with M(community.crypto.openssl_csr).
- The CSR may contain multiple Subject Alternate Names, but each one will lead to an individual challenge that must
be fulfilled for the CSR to be signed.
- 'B(Note): the private key used to create the CSR B(must not) be the account key. This is a bad idea from a security
point of view, and the CA should not accept the CSR. The ACME server should return an error in this case.'
- Precisely one of O(csr) or O(csr_content) must be specified.
type: path
csr_content:
description:
- Content of the CSR for the new certificate.
- Can be created with M(community.crypto.openssl_csr_pipe).
- The CSR may contain multiple Subject Alternate Names, but each one will lead to an individual challenge that must
be fulfilled for the CSR to be signed.
- 'B(Note): the private key used to create the CSR B(must not) be the account key. This is a bad idea from a security
point of view, and the CA should not accept the CSR. The ACME server should return an error in this case.'
- Precisely one of O(csr) or O(csr_content) must be specified.
type: str
"""

View File

@@ -0,0 +1,99 @@
# Copyright (c) Ansible Project
# 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
# Note that this doc fragment is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
class ModuleDocFragment:
# Standard documentation fragment
DOCUMENTATION = r"""
options: {}
attributes:
check_mode:
description: Can run in C(check_mode) and return changed status prediction without modifying target.
diff_mode:
description: Will return details on what has changed (or possibly needs changing in C(check_mode)), when in diff mode.
idempotent:
description:
- When run twice in a row outside check mode, with the same arguments, the second invocation indicates no change.
- This assumes that the system controlled/queried by the module has not changed in a relevant way.
"""
# Should be used together with the standard fragment
IDEMPOTENT_NOT_MODIFY_STATE = r"""
options: {}
attributes:
idempotent:
support: full
details:
- This action does not modify state.
"""
# Should be used together with the standard fragment
INFO_MODULE = r"""
options: {}
attributes:
check_mode:
support: full
details:
- This action does not modify state.
diff_mode:
support: N/A
details:
- This action does not modify state.
"""
ACTIONGROUP_ACME = r"""
options: {}
attributes:
action_group:
description: Use C(group/acme) or C(group/community.crypto.acme) in C(module_defaults) to set defaults for this module.
support: full
membership:
- community.crypto.acme
- acme
"""
FACTS = r"""
options: {}
attributes:
facts:
description: Action returns an C(ansible_facts) dictionary that will update existing host facts.
"""
# Should be used together with the standard fragment and the FACTS fragment
FACTS_MODULE = r"""
options: {}
attributes:
check_mode:
support: full
details:
- This action does not modify state.
diff_mode:
support: N/A
details:
- This action does not modify state.
facts:
support: full
"""
FILES = r"""
options: {}
attributes:
safe_file_operations:
description: Uses Ansible's strict file operation functions to ensure proper permissions and avoid data corruption.
"""
FLOW = r"""
options: {}
attributes:
action:
description: Indicates this has a corresponding action plugin so some parts of the options can be executed on the controller.
async:
description: Supports being used with the C(async) keyword.
"""

View File

@@ -0,0 +1,23 @@
# Copyright (c) 2025 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
# Note that this doc fragment is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
class ModuleDocFragment:
"""
Doc fragments for cryptography requirements.
Must be kept in sync with plugins/module_utils/_cryptography_dep.py.
"""
# Corresponds to the plugins.module_utils._cryptography_dep.COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION constant
MINIMUM = r"""
requirements:
- cryptography >= 3.3
options: {}
"""

View File

@@ -0,0 +1,44 @@
# Copyright (c), Entrust Datacard Corporation, 2019
# 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
# Note that this doc fragment is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
class ModuleDocFragment:
# Plugin options for Entrust Certificate Services (ECS) credentials
DOCUMENTATION = r"""
options:
entrust_api_user:
description:
- The username for authentication to the Entrust Certificate Services (ECS) API.
type: str
required: true
entrust_api_key:
description:
- The key (password) for authentication to the Entrust Certificate Services (ECS) API.
type: str
required: true
entrust_api_client_cert_path:
description:
- The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
type: path
required: true
entrust_api_client_cert_key_path:
description:
- The path to the key for the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
type: path
required: true
entrust_api_specification_path:
description:
- The path to the specification file defining the Entrust Certificate Services (ECS) API configuration.
- You can use this to keep a local copy of the specification to avoid downloading it every time the module is used.
type: path
default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
requirements:
- "PyYAML >= 3.11"
"""

View File

@@ -0,0 +1,417 @@
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# GNU General Public License v3.0+ (see 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
# Note that this doc fragment is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
class ModuleDocFragment:
# Standard files documentation fragment
DOCUMENTATION = r"""
description:
- This module allows one to (re)generate OpenSSL certificates.
- It uses the cryptography python library to interact with OpenSSL.
attributes:
diff_mode:
support: full
idempotent:
support: partial
details:
- If relative timestamps are used and O(ignore_timestamps=false), the module is not idempotent.
- The option O(force=true) generally disables idempotency.
requirements:
- cryptography >= 3.3 (if using V(selfsigned) or V(ownca) provider)
options:
force:
description:
- Generate the certificate, even if it already exists.
type: bool
default: false
csr_path:
description:
- Path to the Certificate Signing Request (CSR) used to generate this certificate.
- This is mutually exclusive with O(csr_content).
type: path
csr_content:
description:
- Content of the Certificate Signing Request (CSR) used to generate this certificate.
- This is mutually exclusive with O(csr_path).
type: str
privatekey_path:
description:
- Path to the private key to use when signing the certificate.
- This is mutually exclusive with O(privatekey_content).
type: path
privatekey_content:
description:
- Content of the private key to use when signing the certificate.
- This is mutually exclusive with O(privatekey_path).
type: str
privatekey_passphrase:
description:
- The passphrase for the O(privatekey_path) resp. O(privatekey_content).
- This is required if the private key is password protected.
type: str
ignore_timestamps:
description:
- Whether the "not before" and "not after" timestamps should be ignored for idempotency checks.
- It is better to keep the default value V(true) when using relative timestamps (like V(+0s) for now).
type: bool
default: true
version_added: 2.0.0
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is V(auto), which tries to use C(cryptography) if available.
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
- Note that with community.crypto 3.0.0, all values behave the same.
This option will be deprecated in a later version.
We recommend to not set it explicitly.
type: str
default: auto
choices: [auto, cryptography]
notes:
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
- Date specified should be UTC. Minutes and seconds are mandatory.
- For security reason, when you use V(ownca) provider, you should NOT run M(community.crypto.x509_certificate) on a target
machine, but on a dedicated CA machine. It is recommended not to store the CA private key on the target machine. Once
signed, the certificate can be moved to the target machine.
seealso:
- module: community.crypto.openssl_csr
- module: community.crypto.openssl_csr_pipe
- module: community.crypto.openssl_dhparam
- module: community.crypto.openssl_pkcs12
- module: community.crypto.openssl_privatekey
- module: community.crypto.openssl_privatekey_pipe
- module: community.crypto.openssl_publickey
"""
BACKEND_ACME_DOCUMENTATION = r"""
description:
- This module allows one to (re)generate OpenSSL certificates.
requirements:
- acme-tiny >= 4.0.0 (if using the V(acme) provider)
options:
acme_accountkey_path:
description:
- The path to the accountkey for the V(acme) provider.
- This is only used by the V(acme) provider.
type: path
acme_challenge_path:
description:
- The path to the ACME challenge directory that is served on U(http://<HOST>:80/.well-known/acme-challenge/)
- This is only used by the V(acme) provider.
type: path
acme_chain:
description:
- Include the intermediate certificate to the generated certificate
- This is only used by the V(acme) provider.
- Note that this is only available for older versions of C(acme-tiny).
New versions include the chain automatically, and setting O(acme_chain) to V(true) results in an error.
type: bool
default: false
acme_directory:
description:
- "The ACME directory to use. You can use any directory that supports the ACME protocol, such as Buypass or Let's Encrypt."
- "Let's Encrypt recommends using their staging server while developing jobs. U(https://letsencrypt.org/docs/staging-environment/)."
type: str
default: https://acme-v02.api.letsencrypt.org/directory
"""
BACKEND_ENTRUST_DOCUMENTATION = r"""
options:
entrust_cert_type:
description:
- Specify the type of certificate requested.
- This is only used by the V(entrust) provider.
type: str
default: STANDARD_SSL
choices: [STANDARD_SSL, ADVANTAGE_SSL, UC_SSL, EV_SSL, WILDCARD_SSL, PRIVATE_SSL, PD_SSL, CDS_ENT_LITE, CDS_ENT_PRO, SMIME_ENT]
entrust_requester_email:
description:
- The email of the requester of the certificate (for tracking purposes).
- This is only used by the V(entrust) provider.
- This is required if the provider is V(entrust).
type: str
entrust_requester_name:
description:
- The name of the requester of the certificate (for tracking purposes).
- This is only used by the V(entrust) provider.
- This is required if the provider is V(entrust).
type: str
entrust_requester_phone:
description:
- The phone number of the requester of the certificate (for tracking purposes).
- This is only used by the V(entrust) provider.
- This is required if the provider is V(entrust).
type: str
entrust_api_user:
description:
- The username for authentication to the Entrust Certificate Services (ECS) API.
- This is only used by the V(entrust) provider.
- This is required if the provider is V(entrust).
type: str
entrust_api_key:
description:
- The key (password) for authentication to the Entrust Certificate Services (ECS) API.
- This is only used by the V(entrust) provider.
- This is required if the provider is V(entrust).
type: str
entrust_api_client_cert_path:
description:
- The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
- This is only used by the V(entrust) provider.
- This is required if the provider is V(entrust).
type: path
entrust_api_client_cert_key_path:
description:
- The path to the private key of the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
- This is only used by the V(entrust) provider.
- This is required if the provider is V(entrust).
type: path
entrust_not_after:
description:
- The point in time at which the certificate stops being valid.
- Time can be specified either as relative time or as an absolute timestamp.
- A valid absolute time format is C(ASN.1 TIME) such as V(2019-06-18).
- A valid relative time format is V([+-]timespec) where timespec can be an integer + C([w | d | h | m | s]), such as V(+365d) or V(+32w1d2h)).
- Time will always be interpreted as UTC.
- Note that only the date (day, month, year) is supported for specifying the expiry date of the issued certificate.
- The full date-time is adjusted to EST (GMT -5:00) before issuance, which may result in a certificate with an expiration date one day
earlier than expected if a relative time is used.
- The minimum certificate lifetime is 90 days, and maximum is three years.
- If this value is not specified, the certificate will stop being valid 365 days the date of issue.
- This is only used by the V(entrust) provider.
- Please note that this value is B(not) covered by the O(ignore_timestamps) option.
type: str
default: +365d
entrust_api_specification_path:
description:
- The path to the specification file defining the Entrust Certificate Services (ECS) API configuration.
- You can use this to keep a local copy of the specification to avoid downloading it every time the module is used.
- This is only used by the V(entrust) provider.
type: path
default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
"""
BACKEND_OWNCA_DOCUMENTATION = r"""
description:
- The V(ownca) provider is intended for generating an OpenSSL certificate signed with your own
CA (Certificate Authority) certificate (self-signed certificate).
options:
ownca_path:
description:
- Remote absolute path of the CA (Certificate Authority) certificate.
- This is only used by the V(ownca) provider.
- This is mutually exclusive with O(ownca_content).
type: path
ownca_content:
description:
- Content of the CA (Certificate Authority) certificate.
- This is only used by the V(ownca) provider.
- This is mutually exclusive with O(ownca_path).
type: str
ownca_privatekey_path:
description:
- Path to the CA (Certificate Authority) private key to use when signing the certificate.
- This is only used by the V(ownca) provider.
- This is mutually exclusive with O(ownca_privatekey_content).
type: path
ownca_privatekey_content:
description:
- Content of the CA (Certificate Authority) private key to use when signing the certificate.
- This is only used by the V(ownca) provider.
- This is mutually exclusive with O(ownca_privatekey_path).
type: str
ownca_privatekey_passphrase:
description:
- The passphrase for the O(ownca_privatekey_path) resp. O(ownca_privatekey_content).
- This is only used by the V(ownca) provider.
type: str
ownca_digest:
description:
- The digest algorithm to be used for the V(ownca) certificate.
- This is only used by the V(ownca) provider.
type: str
default: sha256
ownca_version:
description:
- The version of the V(ownca) certificate.
- Nowadays it should almost always be V(3).
- This is only used by the V(ownca) provider.
type: int
default: 3
choices:
- 3
ownca_not_before:
description:
- The point in time the certificate is valid from.
- Time can be specified either as relative time or as absolute timestamp.
- Time will always be interpreted as UTC.
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ C([w | d | h | m | s]) (for example V(+32w1d2h)).
- If this value is not specified, the certificate will start being valid from now.
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
This can be changed by setting the O(ignore_timestamps) option to V(false). Please note that you should
avoid relative timestamps when setting O(ignore_timestamps=false).
- This is only used by the V(ownca) provider.
type: str
default: +0s
ownca_not_after:
description:
- The point in time at which the certificate stops being valid.
- Time can be specified either as relative time or as absolute timestamp.
- Time will always be interpreted as UTC.
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ C([w | d | h | m | s]) (for example V(+32w1d2h)).
- If this value is not specified, the certificate will stop being valid 10 years from now.
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
This can be changed by setting the O(ignore_timestamps) option to V(false). Please note that you should
avoid relative timestamps when setting O(ignore_timestamps=false).
- This is only used by the V(ownca) provider.
- On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer.
Please see U(https://support.apple.com/en-us/HT210176) for more details.
type: str
default: +3650d
ownca_create_subject_key_identifier:
description:
- Whether to create the Subject Key Identifier (SKI) from the public key.
- A value of V(create_if_not_provided) (default) only creates a SKI when the CSR does not
provide one.
- A value of V(always_create) always creates a SKI. If the CSR provides one, that one is
ignored.
- A value of V(never_create) never creates a SKI. If the CSR provides one, that one is used.
- This is only used by the V(ownca) provider.
type: str
choices: [create_if_not_provided, always_create, never_create]
default: create_if_not_provided
ownca_create_authority_key_identifier:
description:
- Create a Authority Key Identifier from the CA's certificate. If the CSR provided
a authority key identifier, it is ignored.
- The Authority Key Identifier is generated from the CA certificate's Subject Key Identifier,
if available. If it is not available, the CA certificate's public key will be used.
- This is only used by the V(ownca) provider.
type: bool
default: true
"""
BACKEND_SELFSIGNED_DOCUMENTATION = r"""
notes:
- 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.
options:
# NOTE: descriptions in options are overwritten, not appended. For that reason, the texts provided
# here for csr_path and csr_content are not visible to the user. That's why this information is
# added to the notes (see above).
# csr_path:
# description:
# - This is optional for the V(selfsigned) provider. If not provided, a certificate
# without any information (Subject, Subject Alternative Names, Key Usage, etc.) is
# created.
# csr_content:
# description:
# - This is optional for the V(selfsigned) provider. If not provided, a certificate
# without any information (Subject, Subject Alternative Names, Key Usage, etc.) is
# created.
selfsigned_version:
description:
- Version of the V(selfsigned) certificate.
- Nowadays it should almost always be V(3).
- This is only used by the V(selfsigned) provider.
type: int
default: 3
choices:
- 3
selfsigned_digest:
description:
- Digest algorithm to be used when self-signing the certificate.
- This is only used by the V(selfsigned) provider.
type: str
default: sha256
selfsigned_not_before:
description:
- The point in time the certificate is valid from.
- Time can be specified either as relative time or as absolute timestamp.
- Time will always be interpreted as UTC.
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ C([w | d | h | m | s]) (for example V(+32w1d2h)).
- If this value is not specified, the certificate will start being valid from now.
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
This can be changed by setting the O(ignore_timestamps) option to V(false). Please note that you should
avoid relative timestamps when setting O(ignore_timestamps=false).
- This is only used by the V(selfsigned) provider.
type: str
default: +0s
aliases:
- selfsigned_notBefore
selfsigned_not_after:
description:
- The point in time at which the certificate stops being valid.
- Time can be specified either as relative time or as absolute timestamp.
- Time will always be interpreted as UTC.
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ C([w | d | h | m | s]) (for example V(+32w1d2h)).
- If this value is not specified, the certificate will stop being valid 10 years from now.
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
This can be changed by setting the O(ignore_timestamps) option to V(false). Please note that you should
avoid relative timestamps when setting O(ignore_timestamps=false).
- This is only used by the V(selfsigned) provider.
- On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer.
Please see U(https://support.apple.com/en-us/HT210176) for more details.
type: str
default: +3650d
aliases:
- selfsigned_notAfter
selfsigned_create_subject_key_identifier:
description:
- Whether to create the Subject Key Identifier (SKI) from the public key.
- A value of V(create_if_not_provided) (default) only creates a SKI when the CSR does not
provide one.
- A value of V(always_create) always creates a SKI. If the CSR provides one, that one is
ignored.
- A value of V(never_create) never creates a SKI. If the CSR provides one, that one is used.
- This is only used by the V(selfsigned) provider.
type: str
choices: [create_if_not_provided, always_create, never_create]
default: create_if_not_provided
"""

View File

@@ -0,0 +1,344 @@
# Copyright (c) 2017, Yanis Guenane <yanis+ansible@guenane.org>
# 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
# Note that this doc fragment is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
class ModuleDocFragment:
# Standard files documentation fragment
DOCUMENTATION = r"""
description:
- This module allows one to (re)generate OpenSSL certificate signing requests.
- This module supports the subjectAltName, keyUsage, extendedKeyUsage, basicConstraints and OCSP Must Staple extensions.
attributes:
diff_mode:
support: full
idempotent:
support: full
requirements:
- cryptography >= 3.3
options:
digest:
description:
- The digest used when signing the certificate signing request with the private key.
type: str
default: sha256
privatekey_path:
description:
- The path to the private key to use when signing the certificate signing request.
- Either O(privatekey_path) or O(privatekey_content) must be specified if O(state) is V(present), but not both.
type: path
privatekey_content:
description:
- The content of the private key to use when signing the certificate signing request.
- Either O(privatekey_path) or O(privatekey_content) must be specified if O(state) is V(present), but not both.
type: str
privatekey_passphrase:
description:
- The passphrase for the private key.
- This is required if the private key is password protected.
type: str
version:
description:
- The version of the certificate signing request.
- The only allowed value according to L(RFC 2986,https://tools.ietf.org/html/rfc2986#section-4.1) is 1.
- This option no longer accepts unsupported values since community.crypto 2.0.0.
type: int
default: 1
choices:
- 1
subject:
description:
- Key/value pairs that will be present in the subject name field of the certificate signing request.
- If you need to specify more than one value with the same key, use a list as value.
- If the order of the components is important, use O(subject_ordered).
- Mutually exclusive with O(subject_ordered).
type: dict
subject_ordered:
description:
- A list of dictionaries, where every dictionary must contain one key/value pair. This key/value pair will be present
in the subject name field of the certificate signing request.
- If you want to specify more than one value with the same key in a row, you can use a list as value.
- Mutually exclusive with O(subject), and any other subject field option, such as O(country_name), O(state_or_province_name),
O(locality_name), O(organization_name), O(organizational_unit_name), O(common_name), or O(email_address).
type: list
elements: dict
version_added: 2.0.0
country_name:
description:
- The countryName field of the certificate signing request subject.
type: str
aliases:
- C
- countryName
state_or_province_name:
description:
- The stateOrProvinceName field of the certificate signing request subject.
type: str
aliases:
- ST
- stateOrProvinceName
locality_name:
description:
- The localityName field of the certificate signing request subject.
type: str
aliases:
- L
- localityName
organization_name:
description:
- The organizationName field of the certificate signing request subject.
type: str
aliases:
- O
- organizationName
organizational_unit_name:
description:
- The organizationalUnitName field of the certificate signing request subject.
type: str
aliases:
- OU
- organizationalUnitName
common_name:
description:
- The commonName field of the certificate signing request subject.
type: str
aliases:
- CN
- commonName
email_address:
description:
- The emailAddress field of the certificate signing request subject.
type: str
aliases:
- E
- emailAddress
subject_alt_name:
description:
- Subject Alternative Name (SAN) extension to attach to the certificate signing request.
- Values must be prefixed by their options. (These are C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), C(otherName),
and the ones specific to your CA).
- Note that if no SAN is specified, but a common name, the common name will be added as a SAN except if O(use_common_name_for_san)
is set to V(false).
- More at U(https://tools.ietf.org/html/rfc5280#section-4.2.1.6).
type: list
elements: str
aliases:
- subjectAltName
subject_alt_name_critical:
description:
- Should the subjectAltName extension be considered as critical.
type: bool
default: false
aliases:
- subjectAltName_critical
use_common_name_for_san:
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
specified.
type: bool
default: true
aliases:
- useCommonNameForSAN
key_usage:
description:
- This defines the purpose (for example encipherment, signature, certificate signing) of the key contained in the certificate.
type: list
elements: str
aliases:
- keyUsage
key_usage_critical:
description:
- Should the keyUsage extension be considered as critical.
type: bool
default: false
aliases:
- keyUsage_critical
extended_key_usage:
description:
- Additional restrictions (for example client authentication, server authentication) on the allowed purposes for which
the public key may be used.
type: list
elements: str
aliases:
- extKeyUsage
- extendedKeyUsage
extended_key_usage_critical:
description:
- Should the extkeyUsage extension be considered as critical.
type: bool
default: false
aliases:
- extKeyUsage_critical
- extendedKeyUsage_critical
basic_constraints:
description:
- Indicates basic constraints, such as if the certificate is a CA.
type: list
elements: str
aliases:
- basicConstraints
basic_constraints_critical:
description:
- Should the basicConstraints extension be considered as critical.
type: bool
default: false
aliases:
- basicConstraints_critical
ocsp_must_staple:
description:
- Indicates that the certificate should contain the OCSP Must Staple extension (U(https://tools.ietf.org/html/rfc7633)).
type: bool
default: false
aliases:
- ocspMustStaple
ocsp_must_staple_critical:
description:
- Should the OCSP Must Staple extension be considered as critical.
- Note that according to the RFC, this extension should not be marked as critical, as old clients not knowing about
OCSP Must Staple are required to reject such certificates (see U(https://tools.ietf.org/html/rfc7633#section-4)).
type: bool
default: false
aliases:
- ocspMustStaple_critical
name_constraints_permitted:
description:
- For CA certificates, this specifies a list of identifiers which describe subtrees of names that this CA is allowed
to issue certificates for.
- Values must be prefixed by their options. (That is, C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), C(otherName),
and the ones specific to your CA).
type: list
elements: str
name_constraints_excluded:
description:
- For CA certificates, this specifies a list of identifiers which describe subtrees of names that this CA is B(not)
allowed to issue certificates for.
- Values must be prefixed by their options. (That is, C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), C(otherName),
and the ones specific to your CA).
type: list
elements: str
name_constraints_critical:
description:
- Should the Name Constraints extension be considered as critical.
type: bool
default: false
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is V(auto), which tries to use C(cryptography) if available.
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
- Note that with community.crypto 3.0.0, all values behave the same.
This option will be deprecated in a later version.
We recommend to not set it explicitly.
type: str
default: auto
choices: [auto, cryptography]
create_subject_key_identifier:
description:
- Create the Subject Key Identifier from the public key.
- Please note that commercial CAs can ignore the value, respectively use a value of their own choice instead. Specifying
this option is mostly useful for self-signed certificates or for own CAs.
type: bool
default: false
subject_key_identifier:
description:
- The subject key identifier as a hex string, where two bytes are separated by colons.
- 'Example: V(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33).'
- Please note that commercial CAs ignore this value, respectively use a value of their own choice. Specifying this option
is mostly useful for self-signed certificates or for own CAs.
- Note that this option can only be used if O(create_subject_key_identifier) is V(false).
type: str
authority_key_identifier:
description:
- The authority key identifier as a hex string, where two bytes are separated by colons.
- 'Example: V(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33).'
- Please note that commercial CAs ignore this value, respectively use a value of their own choice. Specifying this option
is mostly useful for self-signed certificates or for own CAs.
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of O(authority_key_identifier), O(authority_cert_issuer)
and O(authority_cert_serial_number) is specified.
type: str
authority_cert_issuer:
description:
- Names that will be present in the authority cert issuer field of the certificate signing request.
- Values must be prefixed by their options. (That is, C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), C(otherName),
and the ones specific to your CA).
- 'Example: V(DNS:ca.example.org).'
- If specified, O(authority_cert_serial_number) must also be specified.
- Please note that commercial CAs ignore this value, respectively use a value of their own choice. Specifying this option
is mostly useful for self-signed certificates or for own CAs.
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of O(authority_key_identifier), O(authority_cert_issuer)
and O(authority_cert_serial_number) is specified.
type: list
elements: str
authority_cert_serial_number:
description:
- The authority cert serial number.
- If specified, O(authority_cert_issuer) must also be specified.
- Please note that commercial CAs ignore this value, respectively use a value of their own choice. Specifying this option
is mostly useful for self-signed certificates or for own CAs.
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of O(authority_key_identifier), O(authority_cert_issuer)
and O(authority_cert_serial_number) is specified.
- This option accepts an B(integer). If you want to provide serial numbers as colon-separated hex strings, such as C(11:22:33),
you need to convert them to an integer with P(community.crypto.parse_serial#filter).
type: int
crl_distribution_points:
description:
- Allows to specify one or multiple CRL distribution points.
type: list
elements: dict
suboptions:
full_name:
description:
- Describes how the CRL can be retrieved.
- Mutually exclusive with O(crl_distribution_points[].relative_name).
- 'Example: V(URI:https://ca.example.com/revocations.crl).'
type: list
elements: str
relative_name:
description:
- Describes how the CRL can be retrieved relative to the CRL issuer.
- Mutually exclusive with O(crl_distribution_points[].full_name).
- 'Example: V(/CN=example.com).'
type: list
elements: str
crl_issuer:
description:
- Information about the issuer of the CRL.
type: list
elements: str
reasons:
description:
- List of reasons that this distribution point can be used for when performing revocation checks.
type: list
elements: str
choices:
- key_compromise
- ca_compromise
- affiliation_changed
- superseded
- cessation_of_operation
- certificate_hold
- privilege_withdrawn
- aa_compromise
version_added: 1.4.0
notes:
- If the certificate signing request already exists it will be checked whether subjectAltName, keyUsage, extendedKeyUsage
and basicConstraints only contain the requested values, whether OCSP Must Staple is as requested, and if the request was
signed by the given private key.
seealso:
- module: community.crypto.x509_certificate
- module: community.crypto.x509_certificate_pipe
- module: community.crypto.openssl_dhparam
- module: community.crypto.openssl_pkcs12
- module: community.crypto.openssl_privatekey
- module: community.crypto.openssl_privatekey_pipe
- module: community.crypto.openssl_publickey
- module: community.crypto.openssl_csr_info
- plugin: community.crypto.parse_serial
plugin_type: filter
"""

View File

@@ -0,0 +1,146 @@
# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
# 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
# Note that this doc fragment is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
class ModuleDocFragment:
# Standard files documentation fragment
DOCUMENTATION = r"""
description:
- One can generate L(RSA,https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29), L(DSA,https://en.wikipedia.org/wiki/Digital_Signature_Algorithm),
L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) or L(EdDSA,https://en.wikipedia.org/wiki/EdDSA) private
keys.
- Keys are generated in PEM format.
attributes:
diff_mode:
support: full
idempotent:
support: partial
details:
- The option O(regenerate=always) generally disables idempotency.
requirements:
- cryptography >= 3.3
options:
size:
description:
- Size (in bits) of the TLS/SSL key to generate.
type: int
default: 4096
type:
description:
- The algorithm used to generate the TLS/SSL private key.
type: str
default: RSA
choices: [DSA, ECC, Ed25519, Ed448, RSA, X25519, X448]
curve:
description:
- Note that not all curves are supported by all versions of C(cryptography).
- For maximal interoperability, V(secp384r1) or V(secp256r1) should be used.
- We use the curve names as defined in the L(IANA registry for TLS,https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8).
- Please note that all curves except V(secp224r1), V(secp256k1), V(secp256r1), V(secp384r1), and V(secp521r1) are discouraged
for new private keys.
type: str
choices:
- secp224r1
- secp256k1
- secp256r1
- secp384r1
- secp521r1
- secp192r1
- brainpoolP256r1
- brainpoolP384r1
- brainpoolP512r1
- sect163k1
- sect163r2
- sect233k1
- sect233r1
- sect283k1
- sect283r1
- sect409k1
- sect409r1
- sect571k1
- sect571r1
passphrase:
description:
- The passphrase for the private key.
type: str
cipher:
description:
- The cipher to encrypt the private key. This is only used when O(passphrase) is provided.
- Must be V(auto).
type: str
default: auto
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is V(auto), which tries to use C(cryptography) if available.
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
- Note that with community.crypto 3.0.0, all values behave the same.
This option will be deprecated in a later version.
We recommend to not set it explicitly.
type: str
default: auto
choices: [auto, cryptography]
format:
description:
- Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format) is used for
all keys which support it. Please note that not every key can be exported in any format.
- The value V(auto) selects a format based on the key format. The value V(auto_ignore) does the same, but for existing
private key files, it will not force a regenerate when its format is not the automatically selected one for generation.
- Note that if the format for an existing private key mismatches, the key is B(regenerated) by default. To change this
behavior, use the O(format_mismatch) option.
type: str
default: auto_ignore
choices: [pkcs1, pkcs8, raw, auto, auto_ignore]
format_mismatch:
description:
- Determines behavior of the module if the format of a private key does not match the expected format, but all other
parameters are as expected.
- If set to V(regenerate) (default), generates a new private key.
- If set to V(convert), the key will be converted to the new format instead.
type: str
default: regenerate
choices: [regenerate, convert]
regenerate:
description:
- Allows to configure in which situations the module is allowed to regenerate private keys. The module will always generate
a new key if the destination file does not exist.
- By default, the key will be regenerated when it does not match the module's options, except when the key cannot be
read or the passphrase does not match. Please note that this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior
was as if V(full_idempotence) is specified.
- If set to V(never), the module will fail if the key cannot be read or the passphrase is not matching, and will never
regenerate an existing key.
- If set to V(fail), the module will fail if the key does not correspond to the module's options.
- If set to V(partial_idempotence), the key will be regenerated if it does not conform to the module's options. The
key is B(not) regenerated if it cannot be read (broken file), the key is protected by an unknown passphrase, or when
they key is not protected by a passphrase, but a passphrase is specified.
- If set to V(full_idempotence), the key will be regenerated if it does not conform to the module's options. This is
also the case if the key cannot be read (broken file), the key is protected by an unknown passphrase, or when they
key is not protected by a passphrase, but a passphrase is specified. Make sure you have a B(backup) when using this
option!
- If set to V(always), the module will always regenerate the key. This is equivalent to setting O(force) to V(true).
- Note that if O(format_mismatch) is set to V(convert) and everything matches except the format, the key will always
be converted, except if O(regenerate) is set to V(always).
type: str
choices:
- never
- fail
- partial_idempotence
- full_idempotence
- always
default: full_idempotence
seealso:
- module: community.crypto.x509_certificate
- module: community.crypto.x509_certificate_pipe
- module: community.crypto.openssl_csr
- module: community.crypto.openssl_csr_pipe
- module: community.crypto.openssl_dhparam
- module: community.crypto.openssl_pkcs12
- module: community.crypto.openssl_publickey
"""

View File

@@ -0,0 +1,52 @@
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# 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
# Note that this doc fragment is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
class ModuleDocFragment:
# Standard files documentation fragment
DOCUMENTATION = r"""
requirements:
- cryptography >= 3.3
attributes:
diff_mode:
support: none
idempotent:
support: full
options:
src_path:
description:
- Name of the file containing the OpenSSL private key to convert.
- Exactly one of O(src_path) or O(src_content) must be specified.
type: path
src_content:
description:
- The content of the file containing the OpenSSL private key to convert.
- Exactly one of O(src_path) or O(src_content) must be specified.
type: str
src_passphrase:
description:
- The passphrase for the private key to load.
type: str
dest_passphrase:
description:
- The passphrase for the private key to store.
type: str
format:
description:
- Determines which format the destination private key should be written in.
- Please note that not every key can be exported in any format, and that not every format supports encryption.
type: str
choices: [pkcs1, pkcs8, raw]
required: true
seealso:
- module: community.crypto.openssl_privatekey
- module: community.crypto.openssl_privatekey_pipe
- module: community.crypto.openssl_publickey
"""

View File

@@ -0,0 +1,32 @@
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# 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
# Note that this doc fragment is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
class ModuleDocFragment:
DOCUMENTATION = r"""
options:
name_encoding:
description:
- How to encode names (DNS names, URIs, email addresses) in return values.
- V(ignore) will use the encoding returned by the backend.
- V(idna) will convert all labels of domain names to IDNA encoding. IDNA2008 will be preferred, and IDNA2003 will be
used if IDNA2008 encoding fails.
- V(unicode) will convert all labels of domain names to Unicode. IDNA2008 will be preferred, and IDNA2003 will be used
if IDNA2008 decoding fails.
- B(Note) that V(idna) and V(unicode) require the L(idna Python library,https://pypi.org/project/idna/) to be installed.
type: str
default: ignore
choices:
- ignore
- idna
- unicode
requirements:
- If O(name_encoding) is set to another value than V(ignore), the L(idna Python library,https://pypi.org/project/idna/)
needs to be installed.
"""

View File

@@ -1,139 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class ModuleDocFragment(object):
# Standard files documentation fragment
DOCUMENTATION = r'''
notes:
- "If a new enough version of the C(cryptography) library
is available (see Requirements for details), it will be used
instead of the C(openssl) binary. This can be explicitly disabled
or enabled with the C(select_crypto_backend) option. Note that using
the C(openssl) binary will be slower and less secure, as private key
contents always have to be stored on disk (see
C(account_key_content))."
- "Although the defaults are chosen so that the module can be used with
the L(Let's Encrypt,https://letsencrypt.org/) CA, 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 (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 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."
requirements:
- either openssl or L(cryptography,https://cryptography.io/) >= 1.5
- ipaddress
options:
account_key_src:
description:
- "Path to a file containing the ACME account RSA or Elliptic Curve
key."
- "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 line tool:
RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys
can be created with C(openssl ecparam -genkey ...). Any other tool creating
private keys in PEM format can be used as well."
- "Mutually exclusive with C(account_key_content)."
- "Required if C(account_key_content) is not used."
type: path
aliases: [ account_key ]
account_key_content:
description:
- "Content of the ACME account RSA or Elliptic Curve key."
- "Mutually exclusive with C(account_key_src)."
- "Required if C(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. Since this is an
important private key — it can be used to change the account key,
or to revoke your certificates without knowing their private keys
—, this might not be acceptable."
- "In case C(cryptography) is used, the content is not written into a
temporary file. It can still happen that it is written to disk by
Ansible in the process of moving the module with its argument to
the node where it is executed."
type: str
account_key_passphrase:
description:
- Phassphrase to use to decode the account key.
- "B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend."
type: str
version_added: 1.6.0
account_uri:
description:
- "If specified, assumes that the account URI is as given. If the
account key does not match this account, or an account with this
URI does not exist, the module fails."
type: str
acme_version:
description:
- "The ACME version of the endpoint."
- "Must be C(1) for the classic Let's Encrypt and Buypass ACME endpoints,
or C(2) for standardized ACME v2 endpoints."
- "The value C(1) is deprecated since community.crypto 2.0.0 and will be
removed from community.crypto 3.0.0."
required: true
type: int
choices: [ 1, 2 ]
acme_directory:
description:
- "The ACME directory to use. This is the entry point URL to access
the ACME CA server API."
- "For safety reasons the default is set to the Let's Encrypt staging
server (for the ACME v1 protocol). This will create technically correct,
but untrusted certificates."
- "For Let's Encrypt, all staging endpoints can be found here:
U(https://letsencrypt.org/docs/staging-environment/). For Buypass, all
endpoints can be found here:
U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)"
- "For B(Let's Encrypt), the production directory URL for ACME v2 is
U(https://acme-v02.api.letsencrypt.org/directory)."
- "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(Sectigo), the production directory URL for ACME v2 is
U(https://acme-qa.secure.trust-provider.com/v2/DV)."
- The notes for this module contain a list of ACME services this module has
been tested against.
required: true
type: str
validate_certs:
description:
- Whether calls to the ACME directory will validate TLS certificates.
- "B(Warning:) Should B(only ever) be set to C(false) for testing purposes,
for example when testing against a local Pebble server."
type: bool
default: true
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to
C(openssl).
- If set to C(openssl), will try to use the C(openssl) binary.
- If set to C(cryptography), will try to use the
L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [ auto, cryptography, openssl ]
request_timeout:
description:
- The time Ansible should wait for a response from the ACME API.
- This timeout is applied to all HTTP(S) requests (HEAD, GET, POST).
type: int
default: 10
version_added: 2.3.0
'''

View File

@@ -1,85 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) Ansible Project
# 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
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
class ModuleDocFragment(object):
# Standard documentation fragment
DOCUMENTATION = r'''
options: {}
attributes:
check_mode:
description: Can run in C(check_mode) and return changed status prediction without modifying target.
diff_mode:
description: Will return details on what has changed (or possibly needs changing in C(check_mode)), when in diff mode.
'''
# Should be used together with the standard fragment
INFO_MODULE = r'''
options: {}
attributes:
check_mode:
support: full
details:
- This action does not modify state.
diff_mode:
support: N/A
details:
- This action does not modify state.
'''
ACTIONGROUP_ACME = r'''
options: {}
attributes:
action_group:
description: Use C(group/acme) or C(group/community.crypto.acme) in C(module_defaults) to set defaults for this module.
support: full
membership:
- community.crypto.acme
- acme
'''
FACTS = r'''
options: {}
attributes:
facts:
description: Action returns an C(ansible_facts) dictionary that will update existing host facts.
'''
# Should be used together with the standard fragment and the FACTS fragment
FACTS_MODULE = r'''
options: {}
attributes:
check_mode:
support: full
details:
- This action does not modify state.
diff_mode:
support: N/A
details:
- This action does not modify state.
facts:
support: full
'''
FILES = r'''
options: {}
attributes:
safe_file_operations:
description: Uses Ansible's strict file operation functions to ensure proper permissions and avoid data corruption.
'''
FLOW = r'''
options: {}
attributes:
action:
description: Indicates this has a corresponding action plugin so some parts of the options can be executed on the controller.
async:
description: Supports being used with the C(async) keyword.
'''

View File

@@ -1,44 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c), Entrust Datacard Corporation, 2019
# 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
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
class ModuleDocFragment(object):
# Plugin options for Entrust Certificate Services (ECS) credentials
DOCUMENTATION = r'''
options:
entrust_api_user:
description:
- The username for authentication to the Entrust Certificate Services (ECS) API.
type: str
required: true
entrust_api_key:
description:
- The key (password) for authentication to the Entrust Certificate Services (ECS) API.
type: str
required: true
entrust_api_client_cert_path:
description:
- The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
type: path
required: true
entrust_api_client_cert_key_path:
description:
- The path to the key for the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
type: path
required: true
entrust_api_specification_path:
description:
- The path to the specification file defining the Entrust Certificate Services (ECS) API configuration.
- You can use this to keep a local copy of the specification to avoid downloading it every time the module is used.
type: path
default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
requirements:
- "PyYAML >= 3.11"
'''

View File

@@ -1,404 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# GNU General Public License v3.0+ (see 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class ModuleDocFragment(object):
# Standard files documentation fragment
DOCUMENTATION = r'''
description:
- This module allows one to (re)generate OpenSSL certificates.
- It uses the cryptography python library to interact with OpenSSL.
requirements:
- cryptography >= 1.6 (if using C(selfsigned) or C(ownca) provider)
options:
force:
description:
- Generate the certificate, even if it already exists.
type: bool
default: false
csr_path:
description:
- Path to the Certificate Signing Request (CSR) used to generate this certificate.
- This is mutually exclusive with I(csr_content).
type: path
csr_content:
description:
- Content of the Certificate Signing Request (CSR) used to generate this certificate.
- This is mutually exclusive with I(csr_path).
type: str
privatekey_path:
description:
- Path to the private key to use when signing the certificate.
- This is mutually exclusive with I(privatekey_content).
type: path
privatekey_content:
description:
- Content of the private key to use when signing the certificate.
- This is mutually exclusive with I(privatekey_path).
type: str
privatekey_passphrase:
description:
- The passphrase for the I(privatekey_path) resp. I(privatekey_content).
- This is required if the private key is password protected.
type: str
ignore_timestamps:
description:
- Whether the "not before" and "not after" timestamps should be ignored for idempotency checks.
- It is better to keep the default value C(true) when using relative timestamps (like C(+0s) for now).
type: bool
default: true
version_added: 2.0.0
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [ auto, cryptography ]
notes:
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
- Date specified should be UTC. Minutes and seconds are mandatory.
- For security reason, when you use C(ownca) provider, you should NOT run
M(community.crypto.x509_certificate) on a target machine, but on a dedicated CA machine. It
is recommended not to store the CA private key on the target machine. Once signed, the
certificate can be moved to the target machine.
seealso:
- module: community.crypto.openssl_csr
- module: community.crypto.openssl_csr_pipe
- module: community.crypto.openssl_dhparam
- module: community.crypto.openssl_pkcs12
- module: community.crypto.openssl_privatekey
- module: community.crypto.openssl_privatekey_pipe
- module: community.crypto.openssl_publickey
'''
BACKEND_ACME_DOCUMENTATION = r'''
description:
- This module allows one to (re)generate OpenSSL certificates.
requirements:
- acme-tiny >= 4.0.0 (if using the C(acme) provider)
options:
acme_accountkey_path:
description:
- The path to the accountkey for the C(acme) provider.
- This is only used by the C(acme) provider.
type: path
acme_challenge_path:
description:
- The path to the ACME challenge directory that is served on U(http://<HOST>:80/.well-known/acme-challenge/)
- This is only used by the C(acme) provider.
type: path
acme_chain:
description:
- Include the intermediate certificate to the generated certificate
- This is only used by the C(acme) provider.
- Note that this is only available for older versions of C(acme-tiny).
New versions include the chain automatically, and setting I(acme_chain) to C(true) results in an error.
type: bool
default: false
acme_directory:
description:
- "The ACME directory to use. You can use any directory that supports the ACME protocol, such as Buypass or Let's Encrypt."
- "Let's Encrypt recommends using their staging server while developing jobs. U(https://letsencrypt.org/docs/staging-environment/)."
type: str
default: https://acme-v02.api.letsencrypt.org/directory
'''
BACKEND_ENTRUST_DOCUMENTATION = r'''
options:
entrust_cert_type:
description:
- Specify the type of certificate requested.
- This is only used by the C(entrust) provider.
type: str
default: STANDARD_SSL
choices: [ 'STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', 'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT' ]
entrust_requester_email:
description:
- The email of the requester of the certificate (for tracking purposes).
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: str
entrust_requester_name:
description:
- The name of the requester of the certificate (for tracking purposes).
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: str
entrust_requester_phone:
description:
- The phone number of the requester of the certificate (for tracking purposes).
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: str
entrust_api_user:
description:
- The username for authentication to the Entrust Certificate Services (ECS) API.
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: str
entrust_api_key:
description:
- The key (password) for authentication to the Entrust Certificate Services (ECS) API.
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: str
entrust_api_client_cert_path:
description:
- The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: path
entrust_api_client_cert_key_path:
description:
- The path to the private key of the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: path
entrust_not_after:
description:
- The point in time at which the certificate stops being valid.
- Time can be specified either as relative time or as an absolute timestamp.
- A valid absolute time format is C(ASN.1 TIME) such as C(2019-06-18).
- A valid relative time format is C([+-]timespec) where timespec can be an integer + C([w | d | h | m | s]), such as C(+365d) or C(+32w1d2h)).
- Time will always be interpreted as UTC.
- Note that only the date (day, month, year) is supported for specifying the expiry date of the issued certificate.
- The full date-time is adjusted to EST (GMT -5:00) before issuance, which may result in a certificate with an expiration date one day
earlier than expected if a relative time is used.
- The minimum certificate lifetime is 90 days, and maximum is three years.
- If this value is not specified, the certificate will stop being valid 365 days the date of issue.
- This is only used by the C(entrust) provider.
- Please note that this value is B(not) covered by the I(ignore_timestamps) option.
type: str
default: +365d
entrust_api_specification_path:
description:
- The path to the specification file defining the Entrust Certificate Services (ECS) API configuration.
- You can use this to keep a local copy of the specification to avoid downloading it every time the module is used.
- This is only used by the C(entrust) provider.
type: path
default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
'''
BACKEND_OWNCA_DOCUMENTATION = r'''
description:
- The C(ownca) provider is intended for generating an OpenSSL certificate signed with your own
CA (Certificate Authority) certificate (self-signed certificate).
options:
ownca_path:
description:
- Remote absolute path of the CA (Certificate Authority) certificate.
- This is only used by the C(ownca) provider.
- This is mutually exclusive with I(ownca_content).
type: path
ownca_content:
description:
- Content of the CA (Certificate Authority) certificate.
- This is only used by the C(ownca) provider.
- This is mutually exclusive with I(ownca_path).
type: str
ownca_privatekey_path:
description:
- Path to the CA (Certificate Authority) private key to use when signing the certificate.
- This is only used by the C(ownca) provider.
- This is mutually exclusive with I(ownca_privatekey_content).
type: path
ownca_privatekey_content:
description:
- Content of the CA (Certificate Authority) private key to use when signing the certificate.
- This is only used by the C(ownca) provider.
- This is mutually exclusive with I(ownca_privatekey_path).
type: str
ownca_privatekey_passphrase:
description:
- The passphrase for the I(ownca_privatekey_path) resp. I(ownca_privatekey_content).
- This is only used by the C(ownca) provider.
type: str
ownca_digest:
description:
- The digest algorithm to be used for the C(ownca) certificate.
- This is only used by the C(ownca) provider.
type: str
default: sha256
ownca_version:
description:
- The version of the C(ownca) certificate.
- Nowadays it should almost always be C(3).
- This is only used by the C(ownca) provider.
type: int
default: 3
ownca_not_before:
description:
- The point in time the certificate is valid from.
- Time can be specified either as relative time or as absolute timestamp.
- Time will always be interpreted as UTC.
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ C([w | d | h | m | s]) (for example C(+32w1d2h)).
- If this value is not specified, the certificate will start being valid from now.
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
This can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should
avoid relative timestamps when setting I(ignore_timestamps=false).
- This is only used by the C(ownca) provider.
type: str
default: +0s
ownca_not_after:
description:
- The point in time at which the certificate stops being valid.
- Time can be specified either as relative time or as absolute timestamp.
- Time will always be interpreted as UTC.
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ C([w | d | h | m | s]) (for example C(+32w1d2h)).
- If this value is not specified, the certificate will stop being valid 10 years from now.
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
This can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should
avoid relative timestamps when setting I(ignore_timestamps=false).
- This is only used by the C(ownca) provider.
- On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer.
Please see U(https://support.apple.com/en-us/HT210176) for more details.
type: str
default: +3650d
ownca_create_subject_key_identifier:
description:
- Whether to create the Subject Key Identifier (SKI) from the public key.
- A value of C(create_if_not_provided) (default) only creates a SKI when the CSR does not
provide one.
- A value of C(always_create) always creates a SKI. If the CSR provides one, that one is
ignored.
- A value of C(never_create) never creates a SKI. If the CSR provides one, that one is used.
- This is only used by the C(ownca) provider.
- Note that this is only supported if the C(cryptography) backend is used!
type: str
choices: [create_if_not_provided, always_create, never_create]
default: create_if_not_provided
ownca_create_authority_key_identifier:
description:
- Create a Authority Key Identifier from the CA's certificate. If the CSR provided
a authority key identifier, it is ignored.
- The Authority Key Identifier is generated from the CA certificate's Subject Key Identifier,
if available. If it is not available, the CA certificate's public key will be used.
- This is only used by the C(ownca) provider.
- Note that this is only supported if the C(cryptography) backend is used!
type: bool
default: true
'''
BACKEND_SELFSIGNED_DOCUMENTATION = r'''
notes:
- For the C(selfsigned) provider, I(csr_path) and I(csr_content) are optional. If not provided, a
certificate without any information (Subject, Subject Alternative Names, Key Usage, etc.) is created.
options:
# NOTE: descriptions in options are overwritten, not appended. For that reason, the texts provided
# here for csr_path and csr_content are not visible to the user. That's why this information is
# added to the notes (see above).
# csr_path:
# description:
# - This is optional for the C(selfsigned) provider. If not provided, a certificate
# without any information (Subject, Subject Alternative Names, Key Usage, etc.) is
# created.
# csr_content:
# description:
# - This is optional for the C(selfsigned) provider. If not provided, a certificate
# without any information (Subject, Subject Alternative Names, Key Usage, etc.) is
# created.
selfsigned_version:
description:
- Version of the C(selfsigned) certificate.
- Nowadays it should almost always be C(3).
- This is only used by the C(selfsigned) provider.
type: int
default: 3
selfsigned_digest:
description:
- Digest algorithm to be used when self-signing the certificate.
- This is only used by the C(selfsigned) provider.
type: str
default: sha256
selfsigned_not_before:
description:
- The point in time the certificate is valid from.
- Time can be specified either as relative time or as absolute timestamp.
- Time will always be interpreted as UTC.
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ C([w | d | h | m | s]) (for example C(+32w1d2h)).
- If this value is not specified, the certificate will start being valid from now.
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
This can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should
avoid relative timestamps when setting I(ignore_timestamps=false).
- This is only used by the C(selfsigned) provider.
type: str
default: +0s
aliases: [ selfsigned_notBefore ]
selfsigned_not_after:
description:
- The point in time at which the certificate stops being valid.
- Time can be specified either as relative time or as absolute timestamp.
- Time will always be interpreted as UTC.
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ C([w | d | h | m | s]) (for example C(+32w1d2h)).
- If this value is not specified, the certificate will stop being valid 10 years from now.
- Note that this value is B(not used to determine whether an existing certificate should be regenerated).
This can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should
avoid relative timestamps when setting I(ignore_timestamps=false).
- This is only used by the C(selfsigned) provider.
- On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer.
Please see U(https://support.apple.com/en-us/HT210176) for more details.
type: str
default: +3650d
aliases: [ selfsigned_notAfter ]
selfsigned_create_subject_key_identifier:
description:
- Whether to create the Subject Key Identifier (SKI) from the public key.
- A value of C(create_if_not_provided) (default) only creates a SKI when the CSR does not
provide one.
- A value of C(always_create) always creates a SKI. If the CSR provides one, that one is
ignored.
- A value of C(never_create) never creates a SKI. If the CSR provides one, that one is used.
- This is only used by the C(selfsigned) provider.
- Note that this is only supported if the C(cryptography) backend is used!
type: str
choices: [create_if_not_provided, always_create, never_create]
default: create_if_not_provided
'''

View File

@@ -1,325 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Yanis Guenane <yanis+ansible@guenane.org>
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class ModuleDocFragment(object):
# Standard files documentation fragment
DOCUMENTATION = r'''
description:
- This module allows one to (re)generate OpenSSL certificate signing requests.
- This module supports the subjectAltName, keyUsage, extendedKeyUsage, basicConstraints and OCSP Must Staple
extensions.
requirements:
- cryptography >= 1.3
options:
digest:
description:
- The digest used when signing the certificate signing request with the private key.
type: str
default: sha256
privatekey_path:
description:
- The path to the private key to use when signing the certificate signing request.
- Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
type: path
privatekey_content:
description:
- The content of the private key to use when signing the certificate signing request.
- Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
type: str
privatekey_passphrase:
description:
- The passphrase for the private key.
- This is required if the private key is password protected.
type: str
version:
description:
- The version of the certificate signing request.
- "The only allowed value according to L(RFC 2986,https://tools.ietf.org/html/rfc2986#section-4.1)
is 1."
- This option no longer accepts unsupported values since community.crypto 2.0.0.
type: int
default: 1
choices:
- 1
subject:
description:
- Key/value pairs that will be present in the subject name field of the certificate signing request.
- If you need to specify more than one value with the same key, use a list as value.
- If the order of the components is important, use I(subject_ordered).
- Mutually exclusive with I(subject_ordered).
type: dict
subject_ordered:
description:
- A list of dictionaries, where every dictionary must contain one key/value pair. This key/value pair
will be present in the subject name field of the certificate signing request.
- If you want to specify more than one value with the same key in a row, you can use a list as value.
- Mutually exclusive with I(subject), and any other subject field option, such as I(country_name),
I(state_or_province_name), I(locality_name), I(organization_name), I(organizational_unit_name),
I(common_name), or I(email_address).
type: list
elements: dict
version_added: 2.0.0
country_name:
description:
- The countryName field of the certificate signing request subject.
type: str
aliases: [ C, countryName ]
state_or_province_name:
description:
- The stateOrProvinceName field of the certificate signing request subject.
type: str
aliases: [ ST, stateOrProvinceName ]
locality_name:
description:
- The localityName field of the certificate signing request subject.
type: str
aliases: [ L, localityName ]
organization_name:
description:
- The organizationName field of the certificate signing request subject.
type: str
aliases: [ O, organizationName ]
organizational_unit_name:
description:
- The organizationalUnitName field of the certificate signing request subject.
type: str
aliases: [ OU, organizationalUnitName ]
common_name:
description:
- The commonName field of the certificate signing request subject.
type: str
aliases: [ CN, commonName ]
email_address:
description:
- The emailAddress field of the certificate signing request subject.
type: str
aliases: [ E, emailAddress ]
subject_alt_name:
description:
- Subject Alternative Name (SAN) extension to attach to the certificate signing request.
- Values must be prefixed by their options. (These are C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
C(otherName), and the ones specific to your CA).
- Note that if no SAN is specified, but a common name, the common
name will be added as a SAN except if C(useCommonNameForSAN) is
set to I(false).
- More at U(https://tools.ietf.org/html/rfc5280#section-4.2.1.6).
type: list
elements: str
aliases: [ subjectAltName ]
subject_alt_name_critical:
description:
- Should the subjectAltName extension be considered as critical.
type: bool
default: false
aliases: [ subjectAltName_critical ]
use_common_name_for_san:
description:
- If set to C(true), the module will fill the common name in for
C(subject_alt_name) with C(DNS:) prefix if no SAN is specified.
type: bool
default: true
aliases: [ useCommonNameForSAN ]
key_usage:
description:
- This defines the purpose (for example encipherment, signature, certificate signing)
of the key contained in the certificate.
type: list
elements: str
aliases: [ keyUsage ]
key_usage_critical:
description:
- Should the keyUsage extension be considered as critical.
type: bool
default: false
aliases: [ keyUsage_critical ]
extended_key_usage:
description:
- Additional restrictions (for example client authentication, server authentication)
on the allowed purposes for which the public key may be used.
type: list
elements: str
aliases: [ extKeyUsage, extendedKeyUsage ]
extended_key_usage_critical:
description:
- Should the extkeyUsage extension be considered as critical.
type: bool
default: false
aliases: [ extKeyUsage_critical, extendedKeyUsage_critical ]
basic_constraints:
description:
- Indicates basic constraints, such as if the certificate is a CA.
type: list
elements: str
aliases: [ basicConstraints ]
basic_constraints_critical:
description:
- Should the basicConstraints extension be considered as critical.
type: bool
default: false
aliases: [ basicConstraints_critical ]
ocsp_must_staple:
description:
- Indicates that the certificate should contain the OCSP Must Staple
extension (U(https://tools.ietf.org/html/rfc7633)).
type: bool
default: false
aliases: [ ocspMustStaple ]
ocsp_must_staple_critical:
description:
- Should the OCSP Must Staple extension be considered as critical.
- Note that according to the RFC, this extension should not be marked
as critical, as old clients not knowing about OCSP Must Staple
are required to reject such certificates
(see U(https://tools.ietf.org/html/rfc7633#section-4)).
type: bool
default: false
aliases: [ ocspMustStaple_critical ]
name_constraints_permitted:
description:
- For CA certificates, this specifies a list of identifiers which describe
subtrees of names that this CA is allowed to issue certificates for.
- Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
C(otherName) and the ones specific to your CA).
type: list
elements: str
name_constraints_excluded:
description:
- For CA certificates, this specifies a list of identifiers which describe
subtrees of names that this CA is B(not) allowed to issue certificates for.
- Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
C(otherName) and the ones specific to your CA).
type: list
elements: str
name_constraints_critical:
description:
- Should the Name Constraints extension be considered as critical.
type: bool
default: false
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [ auto, cryptography ]
create_subject_key_identifier:
description:
- Create the Subject Key Identifier from the public key.
- "Please note that commercial CAs can ignore the value, respectively use a value of
their own choice instead. Specifying this option is mostly useful for self-signed
certificates or for own CAs."
- Note that this is only supported if the C(cryptography) backend is used!
type: bool
default: false
subject_key_identifier:
description:
- The subject key identifier as a hex string, where two bytes are separated by colons.
- "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)"
- "Please note that commercial CAs ignore this value, respectively use a value of their
own choice. Specifying this option is mostly useful for self-signed certificates
or for own CAs."
- Note that this option can only be used if I(create_subject_key_identifier) is C(false).
- Note that this is only supported if the C(cryptography) backend is used!
type: str
authority_key_identifier:
description:
- The authority key identifier as a hex string, where two bytes are separated by colons.
- "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)"
- "Please note that commercial CAs ignore this value, respectively use a value of their
own choice. Specifying this option is mostly useful for self-signed certificates
or for own CAs."
- Note that this is only supported if the C(cryptography) backend is used!
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier),
I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
type: str
authority_cert_issuer:
description:
- Names that will be present in the authority cert issuer field of the certificate signing request.
- Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
C(otherName) and the ones specific to your CA)
- "Example: C(DNS:ca.example.org)"
- If specified, I(authority_cert_serial_number) must also be specified.
- "Please note that commercial CAs ignore this value, respectively use a value of their
own choice. Specifying this option is mostly useful for self-signed certificates
or for own CAs."
- Note that this is only supported if the C(cryptography) backend is used!
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier),
I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
type: list
elements: str
authority_cert_serial_number:
description:
- The authority cert serial number.
- If specified, I(authority_cert_issuer) must also be specified.
- Note that this is only supported if the C(cryptography) backend is used!
- "Please note that commercial CAs ignore this value, respectively use a value of their
own choice. Specifying this option is mostly useful for self-signed certificates
or for own CAs."
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier),
I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
type: int
crl_distribution_points:
description:
- Allows to specify one or multiple CRL distribution points.
- Only supported by the C(cryptography) backend.
type: list
elements: dict
suboptions:
full_name:
description:
- Describes how the CRL can be retrieved.
- Mutually exclusive with I(relative_name).
- "Example: C(URI:https://ca.example.com/revocations.crl)."
type: list
elements: str
relative_name:
description:
- Describes how the CRL can be retrieved relative to the CRL issuer.
- Mutually exclusive with I(full_name).
- "Example: C(/CN=example.com)."
- Can only be used when cryptography >= 1.6 is installed.
type: list
elements: str
crl_issuer:
description:
- Information about the issuer of the CRL.
type: list
elements: str
reasons:
description:
- List of reasons that this distribution point can be used for when performing revocation checks.
type: list
elements: str
choices:
- key_compromise
- ca_compromise
- affiliation_changed
- superseded
- cessation_of_operation
- certificate_hold
- privilege_withdrawn
- aa_compromise
version_added: 1.4.0
notes:
- If the certificate signing request already exists it will be checked whether subjectAltName,
keyUsage, extendedKeyUsage and basicConstraints only contain the requested values, whether
OCSP Must Staple is as requested, and if the request was signed by the given private key.
seealso:
- module: community.crypto.x509_certificate
- module: community.crypto.x509_certificate_pipe
- module: community.crypto.openssl_dhparam
- module: community.crypto.openssl_pkcs12
- module: community.crypto.openssl_privatekey
- module: community.crypto.openssl_privatekey_pipe
- module: community.crypto.openssl_publickey
- module: community.crypto.openssl_csr_info
'''

View File

@@ -1,151 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class ModuleDocFragment(object):
# Standard files documentation fragment
DOCUMENTATION = r'''
description:
- One can generate L(RSA,https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29),
L(DSA,https://en.wikipedia.org/wiki/Digital_Signature_Algorithm),
L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) or
L(EdDSA,https://en.wikipedia.org/wiki/EdDSA) private keys.
- Keys are generated in PEM format.
- "Please note that the module regenerates private keys if they do not match
the module's options. In particular, if you provide another passphrase
(or specify none), change the keysize, etc., the private key will be
regenerated. If you are concerned that this could B(overwrite your private key),
consider using the I(backup) option."
requirements:
- cryptography >= 1.2.3 (older versions might work as well)
options:
size:
description:
- Size (in bits) of the TLS/SSL key to generate.
type: int
default: 4096
type:
description:
- The algorithm used to generate the TLS/SSL private key.
- Note that C(ECC), C(X25519), C(X448), C(Ed25519) and C(Ed448) require the C(cryptography) backend.
C(X25519) needs cryptography 2.5 or newer, while C(X448), C(Ed25519) and C(Ed448) require
cryptography 2.6 or newer. For C(ECC), the minimal cryptography version required depends on the
I(curve) option.
type: str
default: RSA
choices: [ DSA, ECC, Ed25519, Ed448, RSA, X25519, X448 ]
curve:
description:
- Note that not all curves are supported by all versions of C(cryptography).
- For maximal interoperability, C(secp384r1) or C(secp256r1) should be used.
- We use the curve names as defined in the
L(IANA registry for TLS,https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8).
- Please note that all curves except C(secp224r1), C(secp256k1), C(secp256r1), C(secp384r1) and C(secp521r1)
are discouraged for new private keys.
type: str
choices:
- secp224r1
- secp256k1
- secp256r1
- secp384r1
- secp521r1
- secp192r1
- brainpoolP256r1
- brainpoolP384r1
- brainpoolP512r1
- sect163k1
- sect163r2
- sect233k1
- sect233r1
- sect283k1
- sect283r1
- sect409k1
- sect409r1
- sect571k1
- sect571r1
passphrase:
description:
- The passphrase for the private key.
type: str
cipher:
description:
- The cipher to encrypt the private key. Must be C(auto).
type: str
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [ auto, cryptography ]
format:
description:
- Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format)
is used for all keys which support it. Please note that not every key can be exported in any format.
- The value C(auto) selects a format based on the key format. The value C(auto_ignore) does the same,
but for existing private key files, it will not force a regenerate when its format is not the automatically
selected one for generation.
- Note that if the format for an existing private key mismatches, the key is B(regenerated) by default.
To change this behavior, use the I(format_mismatch) option.
type: str
default: auto_ignore
choices: [ pkcs1, pkcs8, raw, auto, auto_ignore ]
format_mismatch:
description:
- Determines behavior of the module if the format of a private key does not match the expected format, but all
other parameters are as expected.
- If set to C(regenerate) (default), generates a new private key.
- If set to C(convert), the key will be converted to the new format instead.
- Only supported by the C(cryptography) backend.
type: str
default: regenerate
choices: [ regenerate, convert ]
regenerate:
description:
- Allows to configure in which situations the module is allowed to regenerate private keys.
The module will always generate a new key if the destination file does not exist.
- By default, the key will be regenerated when it does not match the module's options,
except when the key cannot be read or the passphrase does not match. Please note that
this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence)
is specified.
- If set to C(never), the module will fail if the key cannot be read or the passphrase
is not matching, and will never regenerate an existing key.
- If set to C(fail), the module will fail if the key does not correspond to the module's
options.
- If set to C(partial_idempotence), the key will be regenerated if it does not conform to
the module's options. The key is B(not) regenerated if it cannot be read (broken file),
the key is protected by an unknown passphrase, or when they key is not protected by a
passphrase, but a passphrase is specified.
- If set to C(full_idempotence), the key will be regenerated if it does not conform to the
module's options. This is also the case if the key cannot be read (broken file), the key
is protected by an unknown passphrase, or when they key is not protected by a passphrase,
but a passphrase is specified. Make sure you have a B(backup) when using this option!
- If set to C(always), the module will always regenerate the key. This is equivalent to
setting I(force) to C(true).
- Note that if I(format_mismatch) is set to C(convert) and everything matches except the
format, the key will always be converted, except if I(regenerate) is set to C(always).
type: str
choices:
- never
- fail
- partial_idempotence
- full_idempotence
- always
default: full_idempotence
seealso:
- module: community.crypto.x509_certificate
- module: community.crypto.x509_certificate_pipe
- module: community.crypto.openssl_csr
- module: community.crypto.openssl_csr_pipe
- module: community.crypto.openssl_dhparam
- module: community.crypto.openssl_pkcs12
- module: community.crypto.openssl_publickey
'''

View File

@@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class ModuleDocFragment(object):
# Standard files documentation fragment
DOCUMENTATION = r'''
requirements:
- cryptography >= 1.2.3 (older versions might work as well)
options:
src_path:
description:
- Name of the file containing the OpenSSL private key to convert.
- Exactly one of I(src_path) or I(src_content) must be specified.
type: path
src_content:
description:
- The content of the file containing the OpenSSL private key to convert.
- Exactly one of I(src_path) or I(src_content) must be specified.
type: str
src_passphrase:
description:
- The passphrase for the private key to load.
type: str
dest_passphrase:
description:
- The passphrase for the private key to store.
type: str
format:
description:
- Determines which format the destination private key should be written in.
- Please note that not every key can be exported in any format, and that not every
format supports encryption.
type: str
choices: [ pkcs1, pkcs8, raw ]
required: true
seealso:
- module: community.crypto.openssl_privatekey
- module: community.crypto.openssl_privatekey_pipe
- module: community.crypto.openssl_publickey
'''

View File

@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class ModuleDocFragment(object):
DOCUMENTATION = r'''
options:
name_encoding:
description:
- How to encode names (DNS names, URIs, email addresses) in return values.
- C(ignore) will use the encoding returned by the backend.
- C(idna) will convert all labels of domain names to IDNA encoding.
IDNA2008 will be preferred, and IDNA2003 will be used if IDNA2008 encoding fails.
- C(unicode) will convert all labels of domain names to Unicode.
IDNA2008 will be preferred, and IDNA2003 will be used if IDNA2008 decoding fails.
- B(Note) that C(idna) and C(unicode) require the L(idna Python library,https://pypi.org/project/idna/) to be installed.
type: str
default: ignore
choices:
- ignore
- idna
- unicode
requirements:
- If I(name_encoding) is set to another value than C(ignore), the L(idna Python library,https://pypi.org/project/idna/) needs to be installed.
'''

View File

@@ -0,0 +1,75 @@
# Copyright (c) 2023, Felix Fontein <felix@fontein.de>
# 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
from __future__ import annotations
DOCUMENTATION = r"""
name: gpg_fingerprint
short_description: Retrieve a GPG fingerprint from a GPG public or private key
author: Felix Fontein (@felixfontein)
version_added: 2.15.0
description:
- Takes the content of a private or public GPG key as input and returns its fingerprint.
options:
_input:
description:
- The content of a GPG public or private key.
type: string
required: true
requirements:
- GnuPG (C(gpg) executable)
seealso:
- plugin: community.crypto.gpg_fingerprint
plugin_type: lookup
"""
EXAMPLES = r"""
---
- name: Show fingerprint of GPG public key
ansible.builtin.debug:
msg: "{{ lookup('file', '/path/to/public_key.gpg') | community.crypto.gpg_fingerprint }}"
"""
RETURN = r"""
_value:
description:
- The fingerprint of the provided public or private GPG key.
type: string
"""
import typing as t
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils._gnupg.cli import (
GPGError,
get_fingerprint_from_bytes,
)
from ansible_collections.community.crypto.plugins.plugin_utils._gnupg import (
PluginGPGRunner,
)
def gpg_fingerprint(gpg_key_content: str | bytes) -> str:
if not isinstance(gpg_key_content, (str, bytes)):
raise AnsibleFilterError(
f"The input for the community.crypto.gpg_fingerprint filter must be a string; got {type(gpg_key_content)} instead"
)
try:
gpg = PluginGPGRunner()
return get_fingerprint_from_bytes(
gpg_runner=gpg, content=to_bytes(gpg_key_content)
)
except GPGError as exc:
raise AnsibleFilterError(str(exc)) from exc
class FilterModule:
"""Ansible jinja2 filters"""
def filters(self) -> dict[str, t.Callable]:
return {
"gpg_fingerprint": gpg_fingerprint,
}

View File

@@ -1,34 +1,35 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022, Felix Fontein <felix@fontein.de> # Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# 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 annotations
__metaclass__ = type
DOCUMENTATION = '''
DOCUMENTATION = r"""
name: openssl_csr_info name: openssl_csr_info
short_description: Retrieve information from OpenSSL Certificate Signing Requests (CSR) short_description: Retrieve information from OpenSSL Certificate Signing Requests (CSR)
version_added: 2.10.0 version_added: 2.10.0
author: author:
- Felix Fontein (@felixfontein) - Felix Fontein (@felixfontein)
description: description:
- Provided an OpenSSL Certificate Signing Requests (CSR), retrieve information. - Provided an OpenSSL Certificate Signing Requests (CSR), retrieve information.
- This is a filter version of the M(community.crypto.openssl_csr_info) module. - This is a filter version of the M(community.crypto.openssl_csr_info) module.
options: options:
_input: _input:
description: description:
- The content of the OpenSSL CSR. - The content of the OpenSSL CSR.
type: string type: string
required: true required: true
extends_documentation_fragment: extends_documentation_fragment:
- community.crypto.name_encoding - community.crypto._name_encoding
seealso: seealso:
- module: community.crypto.openssl_csr_info - module: community.crypto.openssl_csr_info
''' - plugin: community.crypto.to_serial
plugin_type: filter
"""
EXAMPLES = ''' 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: >-
@@ -38,276 +39,287 @@ EXAMPLES = '''
| community.crypto.openssl_csr_info | community.crypto.openssl_csr_info
).subject_alt_name | join(', ') ).subject_alt_name | join(', ')
}} }}
''' """
RETURN = ''' RETURN = r"""
_value: _value:
description: description:
- Information on the certificate. - Information on the certificate.
type: dict type: dict
contains: contains:
signature_valid: signature_valid:
description: description:
- Whether the CSR's signature is valid. - Whether the CSR's signature is valid.
- In case the check returns C(false), the module will fail. - In case the check returns V(false), the module will fail.
returned: success returned: success
type: bool type: bool
basic_constraints: basic_constraints:
description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present. description: Entries in the C(basic_constraints) extension, or V(none) if extension is not present.
returned: success returned: success
type: list type: list
elements: str elements: str
sample: ['CA:TRUE', 'pathlen:1'] sample: ['CA:TRUE', 'pathlen:1']
basic_constraints_critical: basic_constraints_critical:
description: Whether the C(basic_constraints) extension is critical. description: Whether the C(basic_constraints) extension is critical.
returned: success returned: success
type: bool type: bool
extended_key_usage: extended_key_usage:
description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present. description: Entries in the C(extended_key_usage) extension, or V(none) if extension is not present.
returned: success returned: success
type: list type: list
elements: str elements: str
sample: [Biometric Info, DVCS, Time Stamping] sample: [Biometric Info, DVCS, Time Stamping]
extended_key_usage_critical: extended_key_usage_critical:
description: Whether the C(extended_key_usage) extension is critical. description: Whether the C(extended_key_usage) extension is critical.
returned: success returned: success
type: bool type: bool
extensions_by_oid: extensions_by_oid:
description: Returns a dictionary for every extension OID description: Returns a dictionary for every extension OID.
returned: success returned: success
type: dict type: dict
contains: contains:
critical: critical:
description: Whether the extension is critical. description: Whether the extension is critical.
returned: success returned: success
type: bool type: bool
value: value:
description: description:
- The Base64 encoded value (in DER format) of the extension. - The Base64 encoded value (in DER format) of the extension.
- B(Note) that depending on the C(cryptography) version used, it is - B(Note) that depending on the C(cryptography) version used, it is not possible to extract the ASN.1 content
not possible to extract the ASN.1 content of the extension, but only of the extension, but only to provide the re-encoded content of the extension in case it was parsed by C(cryptography).
to provide the re-encoded content of the extension in case it was This should usually result in exactly the same value, except if the original extension value was malformed.
parsed by C(cryptography). This should usually result in exactly the returned: success
same value, except if the original extension value was malformed. type: str
returned: success sample: "MAMCAQU="
type: str sample: {"1.3.6.1.5.5.7.1.24": {"critical": false, "value": "MAMCAQU="}}
sample: "MAMCAQU=" key_usage:
sample: {"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}} description: Entries in the C(key_usage) extension, or V(none) if extension is not present.
key_usage: returned: success
description: Entries in the C(key_usage) extension, or C(none) if extension is not present. type: str
returned: success sample: [Key Agreement, Data Encipherment]
type: str key_usage_critical:
sample: [Key Agreement, Data Encipherment] description: Whether the C(key_usage) extension is critical.
key_usage_critical: returned: success
description: Whether the C(key_usage) extension is critical. type: bool
returned: success subject_alt_name:
type: bool description:
subject_alt_name: - Entries in the C(subject_alt_name) extension, or V(none) if extension is not present.
description: - See O(name_encoding) for how IDNs are handled.
- Entries in the C(subject_alt_name) extension, or C(none) if extension is not present. returned: success
- See I(name_encoding) for how IDNs are handled. type: list
returned: success elements: str
type: list sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
elements: str subject_alt_name_critical:
sample: ["DNS:www.ansible.com", "IP:1.2.3.4"] description: Whether the C(subject_alt_name) extension is critical.
subject_alt_name_critical: returned: success
description: Whether the C(subject_alt_name) extension is critical. type: bool
returned: success ocsp_must_staple:
type: bool description: V(true) if the OCSP Must Staple extension is present, V(none) otherwise.
ocsp_must_staple: returned: success
description: C(true) if the OCSP Must Staple extension is present, C(none) otherwise. type: bool
returned: success ocsp_must_staple_critical:
type: bool description: Whether the C(ocsp_must_staple) extension is critical.
ocsp_must_staple_critical: returned: success
description: Whether the C(ocsp_must_staple) extension is critical. type: bool
returned: success name_constraints_permitted:
type: bool description: List of permitted subtrees to sign certificates for.
name_constraints_permitted: returned: success
description: List of permitted subtrees to sign certificates for. type: list
returned: success elements: str
type: list sample: ['email:.somedomain.com']
elements: str name_constraints_excluded:
sample: ['email:.somedomain.com'] description:
name_constraints_excluded: - List of excluded subtrees the CA cannot sign certificates for.
description: - Is V(none) if extension is not present.
- List of excluded subtrees the CA cannot sign certificates for. - See O(name_encoding) for how IDNs are handled.
- Is C(none) if extension is not present. returned: success
- See I(name_encoding) for how IDNs are handled. type: list
returned: success elements: str
type: list sample: ['email:.com']
elements: str name_constraints_critical:
sample: ['email:.com'] description:
name_constraints_critical: - Whether the C(name_constraints) extension is critical.
description: - Is V(none) if extension is not present.
- Whether the C(name_constraints) extension is critical. returned: success
- Is C(none) if extension is not present. type: bool
returned: success subject:
type: bool description:
subject: - The CSR's subject as a dictionary.
description: - Note that for repeated values, only the last one will be returned.
- The CSR's subject as a dictionary. returned: success
- Note that for repeated values, only the last one will be returned. type: dict
returned: success sample: {"commonName": "www.example.com", "emailAddress": "test@example.com"}
type: dict subject_ordered:
sample: {"commonName": "www.example.com", "emailAddress": "test@example.com"} description: The CSR's subject as an ordered list of tuples.
subject_ordered: returned: success
description: The CSR's subject as an ordered list of tuples. type: list
returned: success elements: list
type: list sample: [["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]
elements: list public_key:
sample: [["commonName", "www.example.com"], ["emailAddress": "test@example.com"]] description: CSR's public key in PEM format.
public_key: returned: success
description: CSR's public key in PEM format type: str
returned: success sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
type: str public_key_type:
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..." description:
public_key_type: - The CSR's public key's type.
description: - One of V(RSA), V(DSA), V(ECC), V(Ed25519), V(X25519), V(Ed448), or V(X448).
- The CSR's public key's type. - Will start with C(unknown) if the key type cannot be determined.
- One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448). returned: success
- Will start with C(unknown) if the key type cannot be determined. type: str
returned: success sample: RSA
type: str public_key_data:
sample: RSA description:
public_key_data: - Public key data. Depends on the public key's type.
description: returned: success
- Public key data. Depends on the public key's type. type: dict
returned: success contains:
type: dict size:
contains: description:
size: - Bit size of modulus (RSA) or prime number (DSA).
description: type: int
- Bit size of modulus (RSA) or prime number (DSA). returned: When RV(_value.public_key_type=RSA) or RV(_value.public_key_type=DSA)
type: int modulus:
returned: When C(public_key_type=RSA) or C(public_key_type=DSA) description:
modulus: - The RSA key's modulus.
description: type: int
- The RSA key's modulus. returned: When RV(_value.public_key_type=RSA)
type: int exponent:
returned: When C(public_key_type=RSA) description:
exponent: - The RSA key's public exponent.
description: type: int
- The RSA key's public exponent. returned: When RV(_value.public_key_type=RSA)
type: int p:
returned: When C(public_key_type=RSA) description:
p: - The C(p) value for DSA.
description: - This is the prime modulus upon which arithmetic takes place.
- The C(p) value for DSA. type: int
- This is the prime modulus upon which arithmetic takes place. returned: When RV(_value.public_key_type=DSA)
type: int q:
returned: When C(public_key_type=DSA) description:
q: - The C(q) value for DSA.
description: - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the multiplicative
- The C(q) value for DSA. group of the prime field used.
- This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the type: int
multiplicative group of the prime field used. returned: When RV(_value.public_key_type=DSA)
type: int g:
returned: When C(public_key_type=DSA) description:
g: - The C(g) value for DSA.
description: - This is the element spanning the subgroup of the multiplicative group of the prime field used.
- The C(g) value for DSA. type: int
- This is the element spanning the subgroup of the multiplicative group of the prime field used. returned: When RV(_value.public_key_type=DSA)
type: int curve:
returned: When C(public_key_type=DSA) description:
curve: - The curve's name for ECC.
description: type: str
- The curve's name for ECC. returned: When RV(_value.public_key_type=ECC)
type: str exponent_size:
returned: When C(public_key_type=ECC) description:
exponent_size: - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
description: type: int
- The maximum number of bits of a private key. This is basically the bit size of the subgroup used. returned: When RV(_value.public_key_type=ECC)
type: int x:
returned: When C(public_key_type=ECC) description:
x: - The C(x) coordinate for the public point on the elliptic curve.
description: type: int
- The C(x) coordinate for the public point on the elliptic curve. returned: When RV(_value.public_key_type=ECC)
type: int y:
returned: When C(public_key_type=ECC) description:
y: - For RV(_value.public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
description: - For RV(_value.public_key_type=DSA), this is the publicly known group element whose discrete logarithm with respect
- For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. to C(g) is the private key.
- For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. type: int
type: int returned: When RV(_value.public_key_type=DSA) or RV(_value.public_key_type=ECC)
returned: When C(public_key_type=DSA) or C(public_key_type=ECC) public_key_fingerprints:
public_key_fingerprints: description:
description: - Fingerprints of CSR's public key.
- Fingerprints of CSR's public key. - For every hash algorithm available, the fingerprint is computed.
- For every hash algorithm available, the fingerprint is computed. returned: success
returned: success type: dict
type: dict sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63', 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..." subject_key_identifier:
subject_key_identifier: description:
description: - The CSR's subject key identifier.
- The CSR's subject key identifier. - The identifier is returned in hexadecimal, with V(:) used to separate bytes.
- The identifier is returned in hexadecimal, with C(:) used to separate bytes. - Is V(none) if the C(SubjectKeyIdentifier) extension is not present.
- Is C(none) if the C(SubjectKeyIdentifier) extension is not present. returned: success
returned: success type: str
type: str sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' authority_key_identifier:
authority_key_identifier: description:
description: - The CSR's authority key identifier.
- The CSR's authority key identifier. - The identifier is returned in hexadecimal, with V(:) used to separate bytes.
- The identifier is returned in hexadecimal, with C(:) used to separate bytes. - Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. returned: success
returned: success type: str
type: str sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' authority_cert_issuer:
authority_cert_issuer: description:
description: - The CSR's authority cert issuer as a list of general names.
- The CSR's authority cert issuer as a list of general names. - Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. - See O(name_encoding) for how IDNs are handled.
- See I(name_encoding) for how IDNs are handled. returned: success
returned: success type: list
type: list elements: str
elements: str sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
sample: ["DNS:www.ansible.com", "IP:1.2.3.4"] authority_cert_serial_number:
authority_cert_serial_number: description:
description: - The CSR's authority cert serial number.
- The CSR's authority cert serial number. - Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. - This return value is an B(integer). If you need the serial numbers as a colon-separated hex string, such as C(11:22:33),
returned: success you need to convert it to that form with P(community.crypto.to_serial#filter).
type: int returned: success
sample: 12345 type: int
''' sample: 12345
"""
import typing as t
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_text
from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError, OpenSSLObjectError,
) )
from ansible_collections.community.crypto.plugins.module_utils._crypto.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(
'''Extract information from X.509 PEM certificate.''' data: str | bytes, name_encoding: t.Literal["ignore", "idna", "unicode"] = "ignore"
if not isinstance(data, string_types): ) -> dict[str, t.Any]:
raise AnsibleFilterError('The community.crypto.openssl_csr_info input must be a text type, not %s' % type(data)) """Extract information from X.509 PEM certificate."""
if not isinstance(name_encoding, string_types): if not isinstance(data, (str, bytes)):
raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding)) raise AnsibleFilterError(
name_encoding = to_native(name_encoding) f"The community.crypto.openssl_csr_info input must be a text type, not {type(data)}"
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) if not isinstance(name_encoding, (str, bytes)):
raise AnsibleFilterError(
f"The name_encoding option must be of a text type, not {type(name_encoding)}"
)
name_encoding = to_text(name_encoding)
if name_encoding not in ("ignore", "idna", "unicode"):
raise AnsibleFilterError(
f'The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "{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=module, content=to_bytes(data), validate_signature=True
)
except OpenSSLObjectError as exc: except OpenSSLObjectError as exc:
raise AnsibleFilterError(to_native(exc)) raise AnsibleFilterError(str(exc)) from exc
class FilterModule(object): class FilterModule:
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self) -> dict[str, t.Callable]:
return { return {
'openssl_csr_info': openssl_csr_info_filter, "openssl_csr_info": openssl_csr_info_filter,
} }

View File

@@ -1,46 +1,44 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022, Felix Fontein <felix@fontein.de> # Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# 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 annotations
__metaclass__ = type
DOCUMENTATION = '''
DOCUMENTATION = r"""
name: openssl_privatekey_info name: openssl_privatekey_info
short_description: Retrieve information from OpenSSL private keys short_description: Retrieve information from OpenSSL private keys
version_added: 2.10.0 version_added: 2.10.0
author: author:
- Felix Fontein (@felixfontein) - Felix Fontein (@felixfontein)
description: description:
- Provided an OpenSSL private keys, retrieve information. - Provided an OpenSSL private keys, retrieve information.
- This is a filter version of the M(community.crypto.openssl_privatekey_info) module. - This is a filter version of the M(community.crypto.openssl_privatekey_info) module.
options: options:
_input: _input:
description: description:
- The content of the OpenSSL private key. - The content of the OpenSSL private key.
type: string type: string
required: true required: true
passphrase: passphrase:
description: description:
- The passphrase for the private key. - The passphrase for the private key.
type: str type: str
return_private_key_data: return_private_key_data:
description: description:
- Whether to return private key data. - Whether to return private key data.
- Only set this to C(true) when you want private information about this key to - Only set this to V(true) when you want private information about this key to be extracted.
be extracted. - B(WARNING:) you have to make sure that private key data is not accidentally logged!
- "B(WARNING:) you have to make sure that private key data is not accidentally logged!" type: bool
type: bool default: false
default: false
extends_documentation_fragment: extends_documentation_fragment:
- community.crypto.name_encoding - community.crypto._name_encoding
seealso: seealso:
- module: community.crypto.openssl_privatekey_info - module: community.crypto.openssl_privatekey_info
''' """
EXAMPLES = ''' 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: >-
@@ -50,144 +48,160 @@ EXAMPLES = '''
| community.crypto.openssl_privatekey_info | community.crypto.openssl_privatekey_info
).subject_alt_name | join(', ') ).subject_alt_name | join(', ')
}} }}
''' """
RETURN = ''' RETURN = r"""
_value: _value:
description: description:
- Information on the certificate. - Information on the certificate.
type: dict type: dict
contains: contains:
public_key: public_key:
description: Private key's public key in PEM format. description: Private key's public key in PEM format.
returned: success returned: success
type: str type: str
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..." sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
public_key_fingerprints: public_key_fingerprints:
description: description:
- Fingerprints of private key's public key. - Fingerprints of private key's public key.
- For every hash algorithm available, the fingerprint is computed. - For every hash algorithm available, the fingerprint is computed.
returned: success returned: success
type: dict type: dict
sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63', sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..." 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
type: type:
description: description:
- The key's type. - The key's type.
- One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448). - One of V(RSA), V(DSA), V(ECC), V(Ed25519), V(X25519), V(Ed448), or V(X448).
- Will start with C(unknown) if the key type cannot be determined. - Will start with V(unknown) if the key type cannot be determined.
returned: success returned: success
type: str type: str
sample: RSA sample: RSA
public_data: public_data:
description: description:
- Public key data. Depends on key type. - Public key data. Depends on key type.
returned: success returned: success
type: dict type: dict
contains: contains:
size: size:
description: description:
- Bit size of modulus (RSA) or prime number (DSA). - Bit size of modulus (RSA) or prime number (DSA).
type: int type: int
returned: When C(type=RSA) or C(type=DSA) returned: When RV(_value.type=RSA) or RV(_value.type=DSA)
modulus: modulus:
description: description:
- The RSA key's modulus. - The RSA key's modulus.
type: int type: int
returned: When C(type=RSA) returned: When RV(_value.type=RSA)
exponent: exponent:
description: description:
- The RSA key's public exponent. - The RSA key's public exponent.
type: int type: int
returned: When C(type=RSA) returned: When RV(_value.type=RSA)
p: p:
description: description:
- The C(p) value for DSA. - The C(p) value for DSA.
- This is the prime modulus upon which arithmetic takes place. - This is the prime modulus upon which arithmetic takes place.
type: int type: int
returned: When C(type=DSA) returned: When RV(_value.type=DSA)
q: q:
description: description:
- The C(q) value for DSA. - The C(q) value for DSA.
- This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the multiplicative
multiplicative group of the prime field used. group of the prime field used.
type: int type: int
returned: When C(type=DSA) returned: When RV(_value.type=DSA)
g: g:
description: description:
- The C(g) value for DSA. - The C(g) value for DSA.
- This is the element spanning the subgroup of the multiplicative group of the prime field used. - This is the element spanning the subgroup of the multiplicative group of the prime field used.
type: int type: int
returned: When C(type=DSA) returned: When RV(_value.type=DSA)
curve: curve:
description: description:
- The curve's name for ECC. - The curve's name for ECC.
type: str type: str
returned: When C(type=ECC) returned: When RV(_value.type=ECC)
exponent_size: exponent_size:
description: description:
- The maximum number of bits of a private key. This is basically the bit size of the subgroup used. - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
type: int type: int
returned: When C(type=ECC) returned: When RV(_value.type=ECC)
x: x:
description: description:
- The C(x) coordinate for the public point on the elliptic curve. - The C(x) coordinate for the public point on the elliptic curve.
type: int type: int
returned: When C(type=ECC) returned: When RV(_value.type=ECC)
y: y:
description: description:
- For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. - For RV(_value.type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
- For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. - For RV(_value.type=DSA), this is the publicly known group element whose discrete logarithm with respect to C(g)
type: int is the private key.
returned: When C(type=DSA) or C(type=ECC) type: int
private_data: returned: When RV(_value.type=DSA) or RV(_value.type=ECC)
description: private_data:
- Private key data. Depends on key type. description:
returned: success and when I(return_private_key_data) is set to C(true) - Private key data. Depends on key type.
type: dict returned: success and when O(return_private_key_data) is set to V(true)
''' type: dict
"""
import typing as t
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_text
from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError, OpenSSLObjectError,
) )
from ansible_collections.community.crypto.plugins.module_utils._crypto.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: str | bytes,
if not isinstance(data, string_types): passphrase: str | bytes | None = None,
raise AnsibleFilterError('The community.crypto.openssl_privatekey_info input must be a text type, not %s' % type(data)) return_private_key_data: bool = False,
if passphrase is not None and not isinstance(passphrase, string_types): ) -> dict[str, t.Any]:
raise AnsibleFilterError('The passphrase option must be a text type, not %s' % type(passphrase)) """Extract information from X.509 PEM certificate."""
if not isinstance(data, (str, bytes)):
raise AnsibleFilterError(
f"The community.crypto.openssl_privatekey_info input must be a text type, not {type(data)}"
)
if passphrase is not None and not isinstance(passphrase, (str, bytes)):
raise AnsibleFilterError(
f"The passphrase option must be a text type, not {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(
f"The return_private_key_data option must be a boolean, not {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=module,
result.pop('key_is_consistent', None) content=to_bytes(data),
passphrase=to_text(passphrase) if passphrase is not None else None,
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) from exc
except OpenSSLObjectError as exc: except OpenSSLObjectError as exc:
raise AnsibleFilterError(to_native(exc)) raise AnsibleFilterError(str(exc)) from exc
class FilterModule(object): class FilterModule:
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self) -> dict[str, t.Callable]:
return { return {
'openssl_privatekey_info': openssl_privatekey_info_filter, "openssl_privatekey_info": openssl_privatekey_info_filter,
} }

View File

@@ -1,32 +1,31 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022, Felix Fontein <felix@fontein.de> # Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# 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 annotations
__metaclass__ = type
DOCUMENTATION = '''
DOCUMENTATION = r"""
name: openssl_publickey_info name: openssl_publickey_info
short_description: Retrieve information from OpenSSL public keys in PEM format short_description: Retrieve information from OpenSSL public keys in PEM format
version_added: 2.10.0 version_added: 2.10.0
author: author:
- Felix Fontein (@felixfontein) - Felix Fontein (@felixfontein)
description: description:
- Provided a public key in OpenSSL PEM format, retrieve information. - Provided a public key in OpenSSL PEM format, retrieve information.
- This is a filter version of the M(community.crypto.openssl_publickey_info) module. - This is a filter version of the M(community.crypto.openssl_publickey_info) module.
options: options:
_input: _input:
description: description:
- The content of the OpenSSL PEM public key. - The content of the OpenSSL PEM public key.
type: string type: string
required: true required: true
seealso: seealso:
- module: community.crypto.openssl_publickey_info - module: community.crypto.openssl_publickey_info
''' """
EXAMPLES = ''' 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: >-
@@ -36,127 +35,130 @@ EXAMPLES = '''
| community.crypto.openssl_publickey_info | community.crypto.openssl_publickey_info
).type ).type
}} }}
''' """
RETURN = ''' RETURN = r"""
_value: _value:
description: description:
- Information on the public key. - Information on the public key.
type: dict type: dict
contains: contains:
fingerprints: fingerprints:
description: description:
- Fingerprints of public key. - Fingerprints of public key.
- For every hash algorithm available, the fingerprint is computed. - For every hash algorithm available, the fingerprint is computed.
returned: success returned: success
type: dict type: dict
sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63', sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..." 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
type: type:
description: description:
- The key's type. - The key's type.
- One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448). - One of V(RSA), V(DSA), V(ECC), V(Ed25519), V(X25519), V(Ed448), or V(X448).
- Will start with C(unknown) if the key type cannot be determined. - Will start with V(unknown) if the key type cannot be determined.
returned: success returned: success
type: str type: str
sample: RSA sample: RSA
public_data: public_data:
description: description:
- Public key data. Depends on key type. - Public key data. Depends on key type.
returned: success returned: success
type: dict type: dict
contains: contains:
size: size:
description: description:
- Bit size of modulus (RSA) or prime number (DSA). - Bit size of modulus (RSA) or prime number (DSA).
type: int type: int
returned: When C(type=RSA) or C(type=DSA) returned: When RV(_value.type=RSA) or RV(_value.type=DSA)
modulus: modulus:
description: description:
- The RSA key's modulus. - The RSA key's modulus.
type: int type: int
returned: When C(type=RSA) returned: When RV(_value.type=RSA)
exponent: exponent:
description: description:
- The RSA key's public exponent. - The RSA key's public exponent.
type: int type: int
returned: When C(type=RSA) returned: When RV(_value.type=RSA)
p: p:
description: description:
- The C(p) value for DSA. - The C(p) value for DSA.
- This is the prime modulus upon which arithmetic takes place. - This is the prime modulus upon which arithmetic takes place.
type: int type: int
returned: When C(type=DSA) returned: When RV(_value.type=DSA)
q: q:
description: description:
- The C(q) value for DSA. - The C(q) value for DSA.
- This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the multiplicative
multiplicative group of the prime field used. group of the prime field used.
type: int type: int
returned: When C(type=DSA) returned: When RV(_value.type=DSA)
g: g:
description: description:
- The C(g) value for DSA. - The C(g) value for DSA.
- This is the element spanning the subgroup of the multiplicative group of the prime field used. - This is the element spanning the subgroup of the multiplicative group of the prime field used.
type: int type: int
returned: When C(type=DSA) returned: When RV(_value.type=DSA)
curve: curve:
description: description:
- The curve's name for ECC. - The curve's name for ECC.
type: str type: str
returned: When C(type=ECC) returned: When RV(_value.type=ECC)
exponent_size: exponent_size:
description: description:
- The maximum number of bits of a private key. This is basically the bit size of the subgroup used. - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
type: int type: int
returned: When C(type=ECC) returned: When RV(_value.type=ECC)
x: x:
description: description:
- The C(x) coordinate for the public point on the elliptic curve. - The C(x) coordinate for the public point on the elliptic curve.
type: int type: int
returned: When C(type=ECC) returned: When RV(_value.type=ECC)
y: y:
description: description:
- For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. - For RV(_value.type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
- For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. - For RV(_value.type=DSA), this is the publicly known group element whose discrete logarithm with respect to C(g)
type: int is the private key.
returned: When C(type=DSA) or C(type=ECC) type: int
''' returned: When RV(_value.type=DSA) or RV(_value.type=ECC)
"""
import typing as t
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
from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError, OpenSSLObjectError,
) )
from ansible_collections.community.crypto.plugins.module_utils._crypto.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: str | bytes) -> dict[str, t.Any]:
'''Extract information from OpenSSL PEM public key.''' """Extract information from OpenSSL PEM public key."""
if not isinstance(data, string_types): if not isinstance(data, (str, bytes)):
raise AnsibleFilterError('The community.crypto.openssl_publickey_info input must be a text type, not %s' % type(data)) raise AnsibleFilterError(
f"The community.crypto.openssl_publickey_info input must be a text type, not {type(data)}"
)
module = FilterModuleMock({}) module = FilterModuleMock({})
try: try:
return get_publickey_info(module, 'cryptography', content=to_bytes(data)) return get_publickey_info(module=module, content=to_bytes(data))
except PublicKeyParseError as exc: except PublicKeyParseError as exc:
raise AnsibleFilterError(exc.error_message) raise AnsibleFilterError(exc.error_message) from exc
except OpenSSLObjectError as exc: except OpenSSLObjectError as exc:
raise AnsibleFilterError(to_native(exc)) raise AnsibleFilterError(str(exc)) from exc
class FilterModule(object): class FilterModule:
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self) -> dict[str, t.Callable]:
return { return {
'openssl_publickey_info': openssl_publickey_info_filter, "openssl_publickey_info": openssl_publickey_info_filter,
} }

View File

@@ -0,0 +1,68 @@
# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
# 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
from __future__ import annotations
DOCUMENTATION = r"""
name: parse_serial
short_description: Convert a serial number as a colon-separated list of hex numbers to an integer
author: Felix Fontein (@felixfontein)
version_added: 2.18.0
description:
- Parses a colon-separated list of hex numbers of the form C(00:11:22:33) and returns the corresponding integer.
options:
_input:
description:
- A serial number represented as a colon-separated list of hex numbers between 0 and 255.
- These numbers are interpreted as the byte presentation of an unsigned integer in network byte order. That is, C(01:00)
is interpreted as the integer 256.
type: string
required: true
seealso:
- plugin: community.crypto.to_serial
plugin_type: filter
"""
EXAMPLES = r"""
---
- name: Parse serial number
ansible.builtin.debug:
msg: "{{ '11:22:33' | community.crypto.parse_serial }}"
"""
RETURN = r"""
_value:
description:
- The serial number as an integer.
type: int
"""
import typing as t
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_text
from ansible_collections.community.crypto.plugins.module_utils._serial import (
parse_serial,
)
def parse_serial_filter(serial_str: str | bytes) -> int:
if not isinstance(serial_str, (str, bytes)):
raise AnsibleFilterError(
f"The input for the community.crypto.parse_serial filter must be a string; got {type(serial_str)} instead"
)
try:
return parse_serial(to_text(serial_str))
except ValueError as exc:
raise AnsibleFilterError(str(exc)) from exc
class FilterModule:
"""Ansible jinja2 filters"""
def filters(self) -> dict[str, t.Callable]:
return {
"parse_serial": parse_serial_filter,
}

View File

@@ -1,64 +1,66 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022, Felix Fontein <felix@fontein.de> # Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# 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 annotations
__metaclass__ = type
DOCUMENTATION = '''
DOCUMENTATION = r"""
name: split_pem name: split_pem
short_description: Split PEM file contents into multiple objects short_description: Split PEM file contents into multiple objects
version_added: 2.10.0 version_added: 2.10.0
author: author:
- Felix Fontein (@felixfontein) - Felix Fontein (@felixfontein)
description: description:
- Split PEM file contents into multiple PEM objects. Comments or invalid parts are ignored. - Split PEM file contents into multiple PEM objects. Comments or invalid parts are ignored.
options: options:
_input: _input:
description: description:
- The PEM contents to split. - The PEM contents to split.
type: string type: string
required: true required: true
''' """
EXAMPLES = ''' EXAMPLES = r"""
---
- name: Print all CA certificates - name: Print all CA certificates
ansible.builtin.debug: ansible.builtin.debug:
msg: '{{ item }}' msg: '{{ item }}'
loop: >- loop: >-
{{ lookup('ansible.builtin.file', '/path/to/ca-bundle.pem') | community.crypto.split_pem }} {{ lookup('ansible.builtin.file', '/path/to/ca-bundle.pem') | community.crypto.split_pem }}
''' """
RETURN = ''' RETURN = r"""
_value: _value:
description: description:
- A list of PEM file contents. - A list of PEM file contents.
type: list type: list
elements: string elements: string
''' """
import typing as t
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_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,
)
def split_pem_filter(data): def split_pem_filter(data: str | bytes) -> list[str]:
'''Split PEM file.''' """Split PEM file."""
if not isinstance(data, string_types): if not isinstance(data, (str, bytes)):
raise AnsibleFilterError('The community.crypto.split_pem input must be a text type, not %s' % type(data)) raise AnsibleFilterError(
f"The community.crypto.split_pem input must be a text type, not {type(data)}"
)
data = to_text(data) return split_pem_list(to_text(data))
return split_pem_list(data)
class FilterModule(object): class FilterModule:
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self) -> dict[str, t.Callable]:
return { return {
'split_pem': split_pem_filter, "split_pem": split_pem_filter,
} }

View File

@@ -0,0 +1,69 @@
# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
# 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
from __future__ import annotations
DOCUMENTATION = r"""
name: to_serial
short_description: Convert an integer to a colon-separated list of hex numbers
author: Felix Fontein (@felixfontein)
version_added: 2.18.0
description:
- Converts an integer to a colon-separated list of hex numbers of the form C(00:11:22:33).
options:
_input:
description:
- The non-negative integer to convert.
type: int
required: true
seealso:
- plugin: community.crypto.to_serial
plugin_type: filter
"""
EXAMPLES = r"""
---
- name: Convert integer to serial number
ansible.builtin.debug:
msg: "{{ 1234567 | community.crypto.to_serial }}"
"""
RETURN = r"""
_value:
description:
- A colon-separated list of hexadecimal numbers.
- Letters are upper-case, and all numbers have exactly two digits.
- The string is never empty. The representation of C(0) is C("00").
type: string
"""
import typing as t
from ansible.errors import AnsibleFilterError
from ansible_collections.community.crypto.plugins.module_utils._serial import to_serial
def to_serial_filter(serial_int: int) -> str:
if not isinstance(serial_int, int):
raise AnsibleFilterError(
f"The input for the community.crypto.to_serial filter must be an integer; got {type(serial_int)} instead"
)
if serial_int < 0:
raise AnsibleFilterError(
"The input for the community.crypto.to_serial filter must not be negative"
)
try:
return to_serial(serial_int)
except ValueError as exc:
raise AnsibleFilterError(str(exc)) from exc
class FilterModule:
"""Ansible jinja2 filters"""
def filters(self) -> dict[str, t.Callable]:
return {
"to_serial": to_serial_filter,
}

View File

@@ -1,34 +1,35 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022, Felix Fontein <felix@fontein.de> # Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# 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 annotations
__metaclass__ = type
DOCUMENTATION = '''
DOCUMENTATION = r"""
name: x509_certificate_info name: x509_certificate_info
short_description: Retrieve information from X.509 certificates in PEM format short_description: Retrieve information from X.509 certificates in PEM format
version_added: 2.10.0 version_added: 2.10.0
author: author:
- Felix Fontein (@felixfontein) - Felix Fontein (@felixfontein)
description: description:
- Provided a X.509 certificate in PEM format, retrieve information. - Provided a X.509 certificate in PEM format, retrieve information.
- This is a filter version of the M(community.crypto.x509_certificate_info) module. - This is a filter version of the M(community.crypto.x509_certificate_info) module.
options: options:
_input: _input:
description: description:
- The content of the X.509 certificate in PEM format. - The content of the X.509 certificate in PEM format.
type: string type: string
required: true required: true
extends_documentation_fragment: extends_documentation_fragment:
- community.crypto.name_encoding - community.crypto._name_encoding
seealso: seealso:
- module: community.crypto.x509_certificate_info - module: community.crypto.x509_certificate_info
''' - plugin: community.crypto.to_serial
plugin_type: filter
"""
EXAMPLES = ''' 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: >-
@@ -38,309 +39,319 @@ EXAMPLES = '''
| community.crypto.x509_certificate_info | community.crypto.x509_certificate_info
).subject_alt_name | join(', ') ).subject_alt_name | join(', ')
}} }}
''' """
RETURN = ''' RETURN = r"""
_value: _value:
description: description:
- Information on the certificate. - Information on the certificate.
type: dict type: dict
contains: contains:
expired: expired:
description: Whether the certificate is expired (in other words, C(notAfter) is in the past). description: Whether the certificate is expired (in other words, C(notAfter) is in the past).
returned: success returned: success
type: bool type: bool
basic_constraints: basic_constraints:
description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present. description: Entries in the C(basic_constraints) extension, or V(none) if extension is not present.
returned: success returned: success
type: list type: list
elements: str elements: str
sample: ["CA:TRUE", "pathlen:1"] sample: ["CA:TRUE", "pathlen:1"]
basic_constraints_critical: basic_constraints_critical:
description: Whether the C(basic_constraints) extension is critical. description: Whether the C(basic_constraints) extension is critical.
returned: success returned: success
type: bool type: bool
extended_key_usage: extended_key_usage:
description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present. description: Entries in the C(extended_key_usage) extension, or V(none) if extension is not present.
returned: success returned: success
type: list type: list
elements: str elements: str
sample: [Biometric Info, DVCS, Time Stamping] sample: [Biometric Info, DVCS, Time Stamping]
extended_key_usage_critical: extended_key_usage_critical:
description: Whether the C(extended_key_usage) extension is critical. description: Whether the C(extended_key_usage) extension is critical.
returned: success returned: success
type: bool type: bool
extensions_by_oid: extensions_by_oid:
description: Returns a dictionary for every extension OID. description: Returns a dictionary for every extension OID.
returned: success returned: success
type: dict type: dict
contains: contains:
critical: critical:
description: Whether the extension is critical. description: Whether the extension is critical.
returned: success returned: success
type: bool type: bool
value: value:
description: description:
- The Base64 encoded value (in DER format) of the extension. - The Base64 encoded value (in DER format) of the extension.
- B(Note) that depending on the C(cryptography) version used, it is - B(Note) that depending on the C(cryptography) version used, it is not possible to extract the ASN.1 content
not possible to extract the ASN.1 content of the extension, but only of the extension, but only to provide the re-encoded content of the extension in case it was parsed by C(cryptography).
to provide the re-encoded content of the extension in case it was This should usually result in exactly the same value, except if the original extension value was malformed.
parsed by C(cryptography). This should usually result in exactly the returned: success
same value, except if the original extension value was malformed. type: str
returned: success sample: "MAMCAQU="
type: str sample: {"1.3.6.1.5.5.7.1.24": {"critical": false, "value": "MAMCAQU="}}
sample: "MAMCAQU=" key_usage:
sample: {"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}} description: Entries in the C(key_usage) extension, or V(none) if extension is not present.
key_usage: returned: success
description: Entries in the C(key_usage) extension, or C(none) if extension is not present. type: str
returned: success sample: [Key Agreement, Data Encipherment]
type: str key_usage_critical:
sample: [Key Agreement, Data Encipherment] description: Whether the C(key_usage) extension is critical.
key_usage_critical: returned: success
description: Whether the C(key_usage) extension is critical. type: bool
returned: success subject_alt_name:
type: bool description:
subject_alt_name: - Entries in the C(subject_alt_name) extension, or V(none) if extension is not present.
description: - See O(name_encoding) for how IDNs are handled.
- Entries in the C(subject_alt_name) extension, or C(none) if extension is not present. returned: success
- See I(name_encoding) for how IDNs are handled. type: list
returned: success elements: str
type: list sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
elements: str subject_alt_name_critical:
sample: ["DNS:www.ansible.com", "IP:1.2.3.4"] description: Whether the C(subject_alt_name) extension is critical.
subject_alt_name_critical: returned: success
description: Whether the C(subject_alt_name) extension is critical. type: bool
returned: success ocsp_must_staple:
type: bool description: V(true) if the OCSP Must Staple extension is present, V(none) otherwise.
ocsp_must_staple: returned: success
description: C(true) if the OCSP Must Staple extension is present, C(none) otherwise. type: bool
returned: success ocsp_must_staple_critical:
type: bool description: Whether the C(ocsp_must_staple) extension is critical.
ocsp_must_staple_critical: returned: success
description: Whether the C(ocsp_must_staple) extension is critical. type: bool
returned: success issuer:
type: bool description:
issuer: - The certificate's issuer.
description: - Note that for repeated values, only the last one will be returned.
- The certificate's issuer. returned: success
- Note that for repeated values, only the last one will be returned. type: dict
returned: success sample: {"organizationName": "Ansible", "commonName": "ca.example.com"}
type: dict issuer_ordered:
sample: {"organizationName": "Ansible", "commonName": "ca.example.com"} description: The certificate's issuer as an ordered list of tuples.
issuer_ordered: returned: success
description: The certificate's issuer as an ordered list of tuples. type: list
returned: success elements: list
type: list sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]]
elements: list subject:
sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]] description:
subject: - The certificate's subject as a dictionary.
description: - Note that for repeated values, only the last one will be returned.
- The certificate's subject as a dictionary. returned: success
- Note that for repeated values, only the last one will be returned. type: dict
returned: success sample: {"commonName": "www.example.com", "emailAddress": "test@example.com"}
type: dict subject_ordered:
sample: {"commonName": "www.example.com", "emailAddress": "test@example.com"} description: The certificate's subject as an ordered list of tuples.
subject_ordered: returned: success
description: The certificate's subject as an ordered list of tuples. type: list
returned: success elements: list
type: list sample: [["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]
elements: list not_after:
sample: [["commonName", "www.example.com"], ["emailAddress": "test@example.com"]] description: C(notAfter) date as ASN.1 TIME.
not_after: returned: success
description: C(notAfter) date as ASN.1 TIME. type: str
returned: success sample: '20190413202428Z'
type: str not_before:
sample: '20190413202428Z' description: C(notBefore) date as ASN.1 TIME.
not_before: returned: success
description: C(notBefore) date as ASN.1 TIME. type: str
returned: success sample: '20190331202428Z'
type: str public_key:
sample: '20190331202428Z' description: Certificate's public key in PEM format.
public_key: returned: success
description: Certificate's public key in PEM format. type: str
returned: success sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
type: str public_key_type:
sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..." description:
public_key_type: - The certificate's public key's type.
description: - One of V(RSA), V(DSA), V(ECC), V(Ed25519), V(X25519), V(Ed448), or V(X448).
- The certificate's public key's type. - Will start with V(unknown) if the key type cannot be determined.
- One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448). returned: success
- Will start with C(unknown) if the key type cannot be determined. type: str
returned: success sample: RSA
type: str public_key_data:
sample: RSA description:
public_key_data: - Public key data. Depends on the public key's type.
description: returned: success
- Public key data. Depends on the public key's type. type: dict
returned: success contains:
type: dict size:
contains: description:
size: - Bit size of modulus (RSA) or prime number (DSA).
description: type: int
- Bit size of modulus (RSA) or prime number (DSA). returned: When RV(_value.public_key_type=RSA) or RV(_value.public_key_type=DSA)
type: int modulus:
returned: When C(public_key_type=RSA) or C(public_key_type=DSA) description:
modulus: - The RSA key's modulus.
description: type: int
- The RSA key's modulus. returned: When RV(_value.public_key_type=RSA)
type: int exponent:
returned: When C(public_key_type=RSA) description:
exponent: - The RSA key's public exponent.
description: type: int
- The RSA key's public exponent. returned: When RV(_value.public_key_type=RSA)
type: int p:
returned: When C(public_key_type=RSA) description:
p: - The C(p) value for DSA.
description: - This is the prime modulus upon which arithmetic takes place.
- The C(p) value for DSA. type: int
- This is the prime modulus upon which arithmetic takes place. returned: When RV(_value.public_key_type=DSA)
type: int q:
returned: When C(public_key_type=DSA) description:
q: - The C(q) value for DSA.
description: - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the multiplicative
- The C(q) value for DSA. group of the prime field used.
- This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the type: int
multiplicative group of the prime field used. returned: When RV(_value.public_key_type=DSA)
type: int g:
returned: When C(public_key_type=DSA) description:
g: - The C(g) value for DSA.
description: - This is the element spanning the subgroup of the multiplicative group of the prime field used.
- The C(g) value for DSA. type: int
- This is the element spanning the subgroup of the multiplicative group of the prime field used. returned: When RV(_value.public_key_type=DSA)
type: int curve:
returned: When C(public_key_type=DSA) description:
curve: - The curve's name for ECC.
description: type: str
- The curve's name for ECC. returned: When RV(_value.public_key_type=ECC)
type: str exponent_size:
returned: When C(public_key_type=ECC) description:
exponent_size: - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
description: type: int
- The maximum number of bits of a private key. This is basically the bit size of the subgroup used. returned: When RV(_value.public_key_type=ECC)
type: int x:
returned: When C(public_key_type=ECC) description:
x: - The C(x) coordinate for the public point on the elliptic curve.
description: type: int
- The C(x) coordinate for the public point on the elliptic curve. returned: When RV(_value.public_key_type=ECC)
type: int y:
returned: When C(public_key_type=ECC) description:
y: - For RV(_value.public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
description: - For RV(_value.public_key_type=DSA), this is the publicly known group element whose discrete logarithm with respect
- For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. to C(g) is the private key.
- For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. type: int
type: int returned: When RV(_value.public_key_type=DSA) or RV(_value.public_key_type=ECC)
returned: When C(public_key_type=DSA) or C(public_key_type=ECC) public_key_fingerprints:
public_key_fingerprints: description:
description: - Fingerprints of certificate's public key.
- Fingerprints of certificate's public key. - For every hash algorithm available, the fingerprint is computed.
- For every hash algorithm available, the fingerprint is computed. returned: success
returned: success type: dict
type: dict sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63', 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..." fingerprints:
fingerprints: description:
description: - Fingerprints of the DER-encoded form of the whole certificate.
- Fingerprints of the DER-encoded form of the whole certificate. - For every hash algorithm available, the fingerprint is computed.
- For every hash algorithm available, the fingerprint is computed. returned: success
returned: success type: dict
type: dict sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63', 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..." signature_algorithm:
signature_algorithm: description: The signature algorithm used to sign the certificate.
description: The signature algorithm used to sign the certificate. returned: success
returned: success type: str
type: str sample: sha256WithRSAEncryption
sample: sha256WithRSAEncryption serial_number:
serial_number: description:
description: The certificate's serial number. - The certificate's serial number.
returned: success - This return value is an B(integer). If you need the serial numbers as a colon-separated hex string, such as C(11:22:33),
type: int you need to convert it to that form with P(community.crypto.to_serial#filter).
sample: 1234 returned: success
version: type: int
description: The certificate version. sample: 1234
returned: success version:
type: int description: The certificate version.
sample: 3 returned: success
subject_key_identifier: type: int
description: sample: 3
- The certificate's subject key identifier. subject_key_identifier:
- The identifier is returned in hexadecimal, with C(:) used to separate bytes. description:
- Is C(none) if the C(SubjectKeyIdentifier) extension is not present. - The certificate's subject key identifier.
returned: success - The identifier is returned in hexadecimal, with V(:) used to separate bytes.
type: str - Is V(none) if the C(SubjectKeyIdentifier) extension is not present.
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' returned: success
authority_key_identifier: type: str
description: sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
- The certificate's authority key identifier. authority_key_identifier:
- The identifier is returned in hexadecimal, with C(:) used to separate bytes. description:
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. - The certificate's authority key identifier.
returned: success - The identifier is returned in hexadecimal, with V(:) used to separate bytes.
type: str - Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' returned: success
authority_cert_issuer: type: str
description: sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
- The certificate's authority cert issuer as a list of general names. authority_cert_issuer:
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. description:
- See I(name_encoding) for how IDNs are handled. - The certificate's authority cert issuer as a list of general names.
returned: success - Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
type: list - See O(name_encoding) for how IDNs are handled.
elements: str returned: success
sample: ["DNS:www.ansible.com", "IP:1.2.3.4"] type: list
authority_cert_serial_number: elements: str
description: sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
- The certificate's authority cert serial number. authority_cert_serial_number:
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. description:
returned: success - The certificate's authority cert serial number.
type: int - Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
sample: 12345 - This return value is an B(integer). If you need the serial numbers as a colon-separated hex string, such as C(11:22:33),
ocsp_uri: you need to convert it to that form with P(community.crypto.to_serial#filter).
description: The OCSP responder URI, if included in the certificate. Will be returned: success
C(none) if no OCSP responder URI is included. type: int
returned: success sample: 12345
type: str ocsp_uri:
issuer_uri: description: The OCSP responder URI, if included in the certificate. Will be V(none) if no OCSP responder URI is included.
description: The Issuer URI, if included in the certificate. Will be returned: success
C(none) if no issuer URI is included. type: str
returned: success issuer_uri:
type: str description: The Issuer URI, if included in the certificate. Will be V(none) if no issuer URI is included.
''' returned: success
type: str
"""
import typing as t
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_text
from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError, OpenSSLObjectError,
) )
from ansible_collections.community.crypto.plugins.module_utils._crypto.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(
'''Extract information from X.509 PEM certificate.''' data: str | bytes, name_encoding: t.Literal["ignore", "idna", "unicode"] = "ignore"
if not isinstance(data, string_types): ) -> dict[str, t.Any]:
raise AnsibleFilterError('The community.crypto.x509_certificate_info input must be a text type, not %s' % type(data)) """Extract information from X.509 PEM certificate."""
if not isinstance(name_encoding, string_types): if not isinstance(data, (str, bytes)):
raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding)) raise AnsibleFilterError(
name_encoding = to_native(name_encoding) f"The community.crypto.x509_certificate_info input must be a text type, not {type(data)}"
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) if not isinstance(name_encoding, (str, bytes)):
raise AnsibleFilterError(
f"The name_encoding option must be of a text type, not {type(name_encoding)}"
)
name_encoding = to_text(name_encoding)
if name_encoding not in ("ignore", "idna", "unicode"):
raise AnsibleFilterError(
f'The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "{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=module, content=to_bytes(data))
except OpenSSLObjectError as exc: except OpenSSLObjectError as exc:
raise AnsibleFilterError(to_native(exc)) raise AnsibleFilterError(str(exc)) from exc
class FilterModule(object): class FilterModule:
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self) -> dict[str, t.Callable]:
return { return {
'x509_certificate_info': x509_certificate_info_filter, "x509_certificate_info": x509_certificate_info_filter,
} }

View File

@@ -1,43 +1,43 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022, Felix Fontein <felix@fontein.de> # Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# 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 annotations
__metaclass__ = type
DOCUMENTATION = '''
DOCUMENTATION = r"""
name: x509_crl_info name: x509_crl_info
short_description: Retrieve information from X.509 CRLs in PEM format short_description: Retrieve information from X.509 CRLs in PEM format
version_added: 2.10.0 version_added: 2.10.0
author: author:
- Felix Fontein (@felixfontein) - Felix Fontein (@felixfontein)
description: description:
- Provided a X.509 crl in PEM format, retrieve information. - Provided a X.509 crl in PEM format, retrieve information.
- This is a filter version of the M(community.crypto.x509_crl_info) module. - This is a filter version of the M(community.crypto.x509_crl_info) module.
options: options:
_input: _input:
description: description:
- The content of the X.509 CRL in PEM format. - The content of the X.509 CRL in PEM format.
type: string type: string
required: true required: true
list_revoked_certificates: list_revoked_certificates:
description: description:
- If set to C(false), the list of revoked certificates is not included in the result. - If set to V(false), the list of revoked certificates is not included in the result.
- This is useful when retrieving information on large CRL files. Enumerating all revoked - This is useful when retrieving information on large CRL files. Enumerating all revoked certificates can take some
certificates can take some time, including serializing the result as JSON, sending it to time, including serializing the result as JSON, sending it to the Ansible controller, and decoding it again.
the Ansible controller, and decoding it again. type: bool
type: bool default: true
default: true version_added: 1.7.0
version_added: 1.7.0
extends_documentation_fragment: extends_documentation_fragment:
- community.crypto.name_encoding - community.crypto._name_encoding
seealso: seealso:
- module: community.crypto.x509_crl_info - module: community.crypto.x509_crl_info
''' - plugin: community.crypto.to_serial
plugin_type: filter
"""
EXAMPLES = ''' 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: >-
@@ -47,150 +47,178 @@ EXAMPLES = '''
| community.crypto.x509_crl_info | community.crypto.x509_crl_info
).issuer.organizationName ).issuer.organizationName
}} }}
''' """
RETURN = ''' RETURN = r"""
_value: _value:
description: description:
- Information on the CRL. - Information on the CRL.
type: dict type: dict
contains: contains:
format: format:
description: description:
- Whether the CRL is in PEM format (C(pem)) or in DER format (C(der)). - Whether the CRL is in PEM format (V(pem)) or in DER format (V(der)).
returned: success returned: success
type: str type: str
sample: pem sample: pem
choices:
- pem
- der
issuer:
description:
- The CRL's issuer.
- Note that for repeated values, only the last one will be returned.
- See O(name_encoding) for how IDNs are handled.
returned: success
type: dict
sample: {"organizationName": "Ansible", "commonName": "ca.example.com"}
issuer_ordered:
description: The CRL's issuer as an ordered list of tuples.
returned: success
type: list
elements: list
sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]]
last_update:
description: The point in time from which this CRL can be trusted as ASN.1 TIME.
returned: success
type: str
sample: '20190413202428Z'
next_update:
description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME.
returned: success
type: str
sample: '20190413202428Z'
digest:
description: The signature algorithm used to sign the CRL.
returned: success
type: str
sample: sha256WithRSAEncryption
revoked_certificates:
description: List of certificates to be revoked.
returned: success if O(list_revoked_certificates=true)
type: list
elements: dict
contains:
serial_number:
description:
- Serial number of the certificate.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string, such as
C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
type: int
sample: 1234
revocation_date:
description: The point in time the certificate was revoked as ASN.1 TIME.
type: str
sample: '20190413202428Z'
issuer: issuer:
description: description:
- The CRL's issuer. - The certificate's issuer.
- Note that for repeated values, only the last one will be returned. - See O(name_encoding) for how IDNs are handled.
- See I(name_encoding) for how IDNs are handled. type: list
returned: success elements: str
type: dict sample: ["DNS:ca.example.org"]
sample: {"organizationName": "Ansible", "commonName": "ca.example.com"} issuer_critical:
issuer_ordered: description: Whether the certificate issuer extension is critical.
description: The CRL's issuer as an ordered list of tuples. type: bool
returned: success sample: false
type: list reason:
elements: list description:
sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]] - The value for the revocation reason extension.
last_update: type: str
description: The point in time from which this CRL can be trusted as ASN.1 TIME. sample: key_compromise
returned: success choices:
type: str - unspecified
sample: '20190413202428Z' - key_compromise
next_update: - ca_compromise
description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME. - affiliation_changed
returned: success - superseded
type: str - cessation_of_operation
sample: '20190413202428Z' - certificate_hold
digest: - privilege_withdrawn
description: The signature algorithm used to sign the CRL. - aa_compromise
returned: success - remove_from_crl
type: str reason_critical:
sample: sha256WithRSAEncryption description: Whether the revocation reason extension is critical.
revoked_certificates: type: bool
description: List of certificates to be revoked. sample: false
returned: success if I(list_revoked_certificates=true) invalidity_date:
type: list description: |-
elements: dict The point in time it was known/suspected that the private key was compromised
contains: or that the certificate otherwise became invalid as ASN.1 TIME.
serial_number: type: str
description: Serial number of the certificate. sample: '20190413202428Z'
type: int invalidity_date_critical:
sample: 1234 description: Whether the invalidity date extension is critical.
revocation_date: type: bool
description: The point in time the certificate was revoked as ASN.1 TIME. sample: false
type: str """
sample: '20190413202428Z'
issuer:
description:
- The certificate's issuer.
- See I(name_encoding) for how IDNs are handled.
type: list
elements: str
sample: ["DNS:ca.example.org"]
issuer_critical:
description: Whether the certificate issuer extension is critical.
type: bool
sample: false
reason:
description:
- The value for the revocation reason extension.
- One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded),
C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and
C(remove_from_crl).
type: str
sample: key_compromise
reason_critical:
description: Whether the revocation reason extension is critical.
type: bool
sample: false
invalidity_date:
description: |
The point in time it was known/suspected that the private key was compromised
or that the certificate otherwise became invalid as ASN.1 TIME.
type: str
sample: '20190413202428Z'
invalidity_date_critical:
description: Whether the invalidity date extension is critical.
type: bool
sample: false
'''
import base64 import base64
import binascii import binascii
import typing as t
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_text
from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError, OpenSSLObjectError,
) )
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.crl_info import (
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 (
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(
'''Extract information from X.509 PEM certificate.''' data: str | bytes,
if not isinstance(data, string_types): name_encoding: t.Literal["ignore", "idna", "unicode"] = "ignore",
raise AnsibleFilterError('The community.crypto.x509_crl_info input must be a text type, not %s' % type(data)) list_revoked_certificates: bool = True,
if not isinstance(name_encoding, string_types): ) -> dict[str, t.Any]:
raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding)) """Extract information from X.509 PEM certificate."""
if not isinstance(data, (str, bytes)):
raise AnsibleFilterError(
f"The community.crypto.x509_crl_info input must be a text type, not {type(data)}"
)
if not isinstance(name_encoding, (str, bytes)):
raise AnsibleFilterError(
f"The name_encoding option must be of a text type, not {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(
name_encoding = to_native(name_encoding) f"The list_revoked_certificates option must be a boolean, not {type(list_revoked_certificates)}"
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) name_encoding = to_text(name_encoding)
if name_encoding not in ("ignore", "idna", "unicode"):
raise AnsibleFilterError(
f'The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "{name_encoding}"'
)
data = to_bytes(data) data_bytes = to_bytes(data)
if not identify_pem_format(data): if not identify_pem_format(data_bytes):
try: try:
data = base64.b64decode(to_native(data)) data_bytes = base64.b64decode(to_text(data_bytes))
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=module,
content=data_bytes,
list_revoked_certificates=list_revoked_certificates,
)
except OpenSSLObjectError as exc: except OpenSSLObjectError as exc:
raise AnsibleFilterError(to_native(exc)) raise AnsibleFilterError(str(exc)) from exc
class FilterModule(object): class FilterModule:
'''Ansible jinja2 filters''' """Ansible jinja2 filters"""
def filters(self): def filters(self) -> dict[str, t.Callable]:
return { return {
'x509_crl_info': x509_crl_info_filter, "x509_crl_info": x509_crl_info_filter,
} }

View File

@@ -0,0 +1,79 @@
# Copyright (c) 2023, Felix Fontein <felix@fontein.de>
# 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
from __future__ import annotations
DOCUMENTATION = r"""
name: gpg_fingerprint
short_description: Retrieve a GPG fingerprint from a GPG public or private key file
author: Felix Fontein (@felixfontein)
version_added: 2.15.0
description:
- Takes a list of filenames pointing to GPG public or private key files. Returns the fingerprints for each of these keys.
options:
_terms:
description:
- A path to a GPG public or private key.
type: list
elements: path
required: true
requirements:
- GnuPG (C(gpg) executable)
seealso:
- plugin: community.crypto.gpg_fingerprint
plugin_type: filter
"""
EXAMPLES = r"""
---
- name: Show fingerprint of GPG public key
ansible.builtin.debug:
msg: "{{ lookup('community.crypto.gpg_fingerprint', '/path/to/public_key.gpg') }}"
"""
RETURN = r"""
_value:
description:
- The fingerprints of the provided public or private GPG keys.
- The list has one entry for every path provided.
type: list
elements: string
"""
import os
import typing as t
from ansible.errors import AnsibleLookupError
from ansible.module_utils.common.text.converters import to_text
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.plugin_utils._gnupg import (
PluginGPGRunner,
)
class LookupModule(LookupBase):
def run(self, terms: list[t.Any], variables=None, **kwargs) -> list[str]:
self.set_options(direct=kwargs)
if self._loader is None:
raise AssertionError("Contract violation: self._loader is None")
try:
gpg = PluginGPGRunner(cwd=self._loader.get_basedir())
result = []
for i, path in enumerate(terms):
if not isinstance(path, (str, bytes, os.PathLike)):
raise AnsibleLookupError(
f"Lookup parameter #{i} should be string or a path object, but got {type(path)}"
)
result.append(
get_fingerprint_from_file(gpg_runner=gpg, path=to_text(path))
)
return result
except GPGError as exc:
raise AnsibleLookupError(str(exc)) from exc

View File

@@ -0,0 +1,346 @@
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import typing as t
from ansible.module_utils.common._collections_compat import Mapping
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
ACMEProtocolException,
ModuleFailException,
)
if t.TYPE_CHECKING:
from ansible_collections.community.crypto.plugins.module_utils._acme.acme import (
ACMEClient,
)
class ACMEAccount:
"""
ACME account object. Allows to create new accounts, check for existence of accounts,
retrieve account data.
"""
def __init__(self, *, client: ACMEClient) -> None:
# Set to true to enable logging of all signed requests
self._debug: bool = False
self.client = client
def _new_reg(
self,
*,
contact: list[str] | None = None,
terms_agreed: bool = False,
allow_creation: bool = True,
external_account_binding: dict[str, t.Any] | None = None,
) -> tuple[bool, dict[str, t.Any] | None]:
"""
Registers a new ACME account. Returns a pair ``(created, data)``.
Here, ``created`` is ``True`` if the account was created and
``False`` if it already existed (e.g. it was not newly created),
or does not exist. In case the account was created or exists,
``data`` contains the account data; otherwise, it is ``None``.
If specified, ``external_account_binding`` should be a dictionary
with keys ``kid``, ``alg`` and ``key``
(https://tools.ietf.org/html/rfc8555#section-7.3.4).
https://tools.ietf.org/html/rfc8555#section-7.3
"""
contact = contact or []
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
# and provide external_account_binding credentials. Thus we first send a request with allow_creation=False
# to see whether the account already exists.
# Note that we pass contact here: ZeroSSL does not accept registration calls without contacts, even
# if onlyReturnExisting is set to true.
created, data = self._new_reg(contact=contact, allow_creation=False)
if data:
# An account already exists! Return data
return created, data
# An account does not yet exist. Try to create one next.
new_reg: dict[str, t.Any] = {"contact": contact}
if not allow_creation:
# https://tools.ietf.org/html/rfc8555#section-7.3.1
new_reg["onlyReturnExisting"] = True
if terms_agreed:
new_reg["termsOfServiceAgreed"] = True
url = self.client.directory["newAccount"]
if external_account_binding is not None:
new_reg["externalAccountBinding"] = self.client.sign_request(
protected={
"alg": external_account_binding["alg"],
"kid": external_account_binding["kid"],
"url": url,
},
payload=self.client.account_jwk,
key_data=self.client.backend.create_mac_key(
alg=external_account_binding["alg"],
key=external_account_binding["key"],
),
)
elif (
self.client.directory["meta"].get("externalAccountRequired")
and allow_creation
):
raise ModuleFailException(
"To create an account, an external account binding must be specified. "
"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
)
if not isinstance(result, Mapping):
raise ACMEProtocolException(
module=self.client.module,
msg="Invalid account creation reply from ACME server",
info=info,
content_json=result,
)
if info["status"] == 201:
# Account did not exist
if "location" in info:
self.client.set_account_uri(info["location"])
return True, result
if info["status"] == 200:
# Account did exist
if result.get("status") == "deactivated":
# A bug in Pebble (https://github.com/letsencrypt/pebble/issues/179) and
# Boulder (https://github.com/letsencrypt/boulder/issues/3971): this should
# not return a valid account object according to
# https://tools.ietf.org/html/rfc8555#section-7.3.6:
# "Once an account is deactivated, the server MUST NOT accept further
# requests authorized by that account's key."
if not allow_creation:
return False, None
raise ModuleFailException("Account is deactivated")
if "location" in info:
self.client.set_account_uri(info["location"])
return False, result
if (
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)
# (According to RFC 8555, Section 7.3.1, the HTTP status code MUST be 400.
# Unfortunately Digicert does not care and sends 404 instead.)
return False, None
if (
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
# implemented for Boulder (https://github.com/letsencrypt/boulder/issues/3971),
# might need adjustment in error detection.
if not allow_creation:
return False, None
raise ModuleFailException("Account is deactivated")
raise ACMEProtocolException(
module=self.client.module,
msg="Registering ACME account failed",
info=info,
content_json=result,
)
def get_account_data(self) -> dict[str, t.Any] | None:
"""
Retrieve account information. Can only be called when the account
URI is already known (such as after calling setup_account).
Return None if the account was deactivated, or a dict otherwise.
"""
if self.client.account_uri is None:
raise ModuleFailException("Account URI unknown")
# try POST-as-GET first (draft-15 or newer)
data: dict[str, t.Any] | None = None
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
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
data = {}
result, info = self.client.send_signed_request(
self.client.account_uri, data, fail_on_error=False
)
if not isinstance(result, Mapping):
raise ACMEProtocolException(
module=self.client.module,
msg="Invalid account data retrieved from ACME server",
info=info,
content_json=result,
)
if (
info["status"] in (400, 403)
and result.get("type") == "urn:ietf:params:acme:error:unauthorized"
):
# Returned when account is deactivated
return None
if (
info["status"] in (400, 404)
and result.get("type") == "urn:ietf:params:acme:error:accountDoesNotExist"
):
# Returned when account does not exist
return None
if info["status"] < 200 or info["status"] >= 300:
raise ACMEProtocolException(
module=self.client.module,
msg="Error retrieving account data",
info=info,
content_json=result,
)
return result
@t.overload
def setup_account(
self,
*,
contact: list[str] | None = None,
terms_agreed: bool = False,
allow_creation: t.Literal[True] = True,
remove_account_uri_if_not_exists: bool = False,
external_account_binding: dict[str, t.Any] | None = None,
) -> tuple[bool, dict[str, t.Any]]: ...
@t.overload
def setup_account(
self,
*,
contact: list[str] | None = None,
terms_agreed: bool = False,
allow_creation: bool = True,
remove_account_uri_if_not_exists: bool = False,
external_account_binding: dict[str, t.Any] | None = None,
) -> tuple[bool, dict[str, t.Any] | None]: ...
def setup_account(
self,
*,
contact: list[str] | None = None,
terms_agreed: bool = False,
allow_creation: bool = True,
remove_account_uri_if_not_exists: bool = False,
external_account_binding: dict[str, t.Any] | None = None,
) -> tuple[bool, dict[str, t.Any] | None]:
"""
Detect or create an account on the ACME server. For ACME v1,
as the only way (without knowing an account URI) to test if an
account exists is to try and create one with the provided account
key, this method will always result in an account being present
(except on error situations). For ACME v2, a new account will
only be created if ``allow_creation`` is set to True.
For ACME v2, ``check_mode`` is fully respected. For ACME v1, the
account might be created if it does not yet exist.
Return a pair ``(created, account_data)``. Here, ``created`` will
be ``True`` in case the account was created or would be created
(check mode). ``account_data`` will be the current account data,
or ``None`` if the account does not exist.
The account URI will be stored in ``client.account_uri``; if it is ``None``,
the account does not exist.
If specified, ``external_account_binding`` should be a dictionary
with keys ``kid``, ``alg`` and ``key``
(https://tools.ietf.org/html/rfc8555#section-7.3.4).
https://tools.ietf.org/html/rfc8555#section-7.3
"""
if self.client.account_uri is not None:
created = False
# Verify that the account key belongs to the URI.
# (If update_contact is True, this will be done below.)
account_data = self.get_account_data()
if account_data is None:
if remove_account_uri_if_not_exists and not allow_creation:
self.client.account_uri = None
else:
raise ModuleFailException(
"Account is deactivated or does not exist!"
)
else:
created, account_data = self._new_reg(
contact=contact,
terms_agreed=terms_agreed,
allow_creation=allow_creation and not self.client.module.check_mode,
external_account_binding=external_account_binding,
)
if (
self.client.module.check_mode
and self.client.account_uri is None
and allow_creation
):
created = True
account_data = {"contact": contact or []}
return created, account_data
def update_account(
self, *, account_data: dict[str, t.Any], contact: list[str] | None = None
) -> tuple[bool, dict[str, t.Any]]:
"""
Update an account on the ACME server. Check mode is fully respected.
The current account data must be provided as ``account_data``.
Return a pair ``(updated, account_data)``, where ``updated`` is
``True`` in case something changed (contact info updated) or
would be changed (check mode), and ``account_data`` the updated
account data.
https://tools.ietf.org/html/rfc8555#section-7.3.2
"""
if self.client.account_uri is None:
raise ModuleFailException("Cannot update account without account URI")
# Create request
update_request: dict[str, t.Any] = {}
if contact is not None and account_data.get("contact", []) != contact:
update_request["contact"] = list(contact)
# No change?
if not update_request:
return False, dict(account_data)
# Apply change
if self.client.module.check_mode:
account_data = dict(account_data)
account_data.update(update_request)
else:
account_data, info = self.client.send_signed_request(
self.client.account_uri, update_request
)
if not isinstance(account_data, Mapping):
raise ACMEProtocolException(
module=self.client.module,
msg="Invalid account updating reply from ACME server",
info=info,
content_json=account_data,
)
return True, account_data
__all__ = ("ACMEAccount",)

View File

@@ -0,0 +1,738 @@
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import copy
import datetime
import json
import locale
import time
import typing as t
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes
from ansible.module_utils.urls import fetch_url
from ansible_collections.community.crypto.plugins.module_utils._acme.backend_cryptography import (
CRYPTOGRAPHY_ERROR,
CRYPTOGRAPHY_MINIMAL_VERSION,
CRYPTOGRAPHY_VERSION,
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 (
ACMEProtocolException,
KeyParsingError,
ModuleFailException,
NetworkException,
format_http_status,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.utils import (
compute_cert_id,
nopad_b64,
parse_retry_after,
)
from ansible_collections.community.crypto.plugins.module_utils._argspec import (
ArgumentSpec,
)
from ansible_collections.community.crypto.plugins.module_utils._time import (
get_now_datetime,
)
if t.TYPE_CHECKING:
import os
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._acme.account import (
ACMEAccount,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.backends import (
CertificateInformation,
CryptoBackend,
)
# -1 usually means connection problems
RETRY_STATUS_CODES = (-1, 408, 429, 503)
RETRY_COUNT = 10
def _decode_retry(
*, module: AnsibleModule, response: t.Any, info: dict[str, t.Any], retry_count: int
) -> bool:
if info["status"] not in RETRY_STATUS_CODES:
return False
if retry_count >= RETRY_COUNT:
raise ACMEProtocolException(
module=module,
msg=f"Giving up after {RETRY_COUNT} retries",
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)
now = get_now_datetime(with_timezone=True)
try:
then = parse_retry_after(
info.get("retry-after", "10"), relative_with_timezone=True, now=now
)
retry_after = (then - now).total_seconds()
retry_after = min(max(1, retry_after), 60)
except (TypeError, ValueError):
retry_after = 10
module.log(
f"Retrieved a {format_http_status(info['status'])} HTTP status on {info['url']}, retrying in {retry_after} seconds"
)
time.sleep(retry_after)
return True
def _assert_fetch_url_success(
*,
module: AnsibleModule,
response: t.Any,
info: dict[str, t.Any],
allow_redirect: bool = False,
allow_client_error: bool = True,
allow_server_error: bool = True,
) -> None:
if info["status"] < 0:
raise NetworkException(msg=f"Failure downloading {info['url']}, {info['msg']}")
if (
(300 <= info["status"] < 400 and not allow_redirect)
or (400 <= info["status"] < 500 and not allow_client_error)
or (info["status"] >= 500 and not allow_server_error)
):
raise ACMEProtocolException(module=module, info=info, response=response)
def _is_failed(
*, info: dict[str, t.Any], expected_status_codes: t.Iterable[int] | None = None
) -> bool:
if info["status"] < 200 or info["status"] >= 400:
return True
if (
expected_status_codes is not None
and info["status"] not in expected_status_codes
):
return True
return False
class ACMEDirectory:
"""
The ACME server directory. Gives access to the available resources,
and allows to obtain a Replay-Nonce. The acme_directory URL
needs to support unauthenticated GET requests; ACME endpoints
requiring authentication are not supported.
https://tools.ietf.org/html/rfc8555#section-7.1.1
"""
def __init__(self, *, module: AnsibleModule, client: ACMEClient) -> None:
self.module = module
self.directory_root = module.params["acme_directory"]
self.version = module.params["acme_version"]
self.directory, dummy = client.get_request(self.directory_root, get_only=True)
self.request_timeout = module.params["request_timeout"]
# Check whether self.version matches what we expect
if self.version == 2:
for key in ("newNonce", "newAccount", "newOrder"):
if key not in self.directory:
raise ModuleFailException(
"ACME directory does not seem to follow protocol ACME v2"
)
# Make sure that 'meta' is always available
if "meta" not in self.directory:
self.directory["meta"] = {}
def __getitem__(self, key: str) -> t.Any:
return self.directory[key]
def __contains__(self, key: str) -> bool:
return key in self.directory
def get(self, key: str, default_value: t.Any = None) -> t.Any:
return self.directory.get(key, default_value)
def get_nonce(self, resource: str | None = None) -> str:
url = self.directory["newNonce"]
if resource is not None:
url = resource
retry_count = 0
while True:
response, info = fetch_url(
self.module, url, method="HEAD", timeout=self.request_timeout
)
if _decode_retry(
module=self.module,
response=response,
info=info,
retry_count=retry_count,
):
retry_count += 1
continue
if info["status"] not in (200, 204):
raise NetworkException(
f"Failed to get replay-nonce, got status {format_http_status(info['status'])}"
)
if "replay-nonce" in info:
return info["replay-nonce"]
self.module.log(
f"HEAD to {url} did return status {format_http_status(info['status'])}, but no replay-nonce header!"
)
if retry_count >= 5:
raise ACMEProtocolException(
module=self.module,
msg="Was not able to obtain nonce, giving up after 5 retries",
info=info,
response=response,
)
retry_count += 1
def has_renewal_info_endpoint(self) -> bool:
return "renewalInfo" in self.directory
class ACMEClient:
"""
ACME client object. Handles the authorized communication with the
ACME server.
"""
def __init__(self, *, module: AnsibleModule, backend: CryptoBackend) -> None:
# Set to true to enable logging of all signed requests
self._debug = False
self.module = module
self.backend = backend
self.version = module.params["acme_version"]
# account_key path and content are mutually exclusive
self.account_key_file = module.params.get("account_key_src")
self.account_key_content = module.params.get("account_key_content")
self.account_key_passphrase = module.params.get("account_key_passphrase")
# Grab account URI from module parameters.
# Make sure empty string is treated as None.
self.account_uri = module.params.get("account_uri") or None
self.request_timeout = module.params["request_timeout"]
self.account_key_data = None
self.account_jwk = None
self.account_jws_header = None
if self.account_key_file is not None or self.account_key_content is not None:
try:
self.account_key_data = self.parse_key(
key_file=self.account_key_file,
key_content=self.account_key_content,
passphrase=self.account_key_passphrase,
)
except KeyParsingError as e:
raise ModuleFailException(
f"Error while parsing account key: {e.msg}"
) from e
self.account_jwk = self.account_key_data["jwk"]
self.account_jws_header = {
"alg": self.account_key_data["alg"],
"jwk": self.account_jwk,
}
if self.account_uri:
# Make sure self.account_jws_header is updated
self.set_account_uri(self.account_uri)
self.directory = ACMEDirectory(module=module, client=self)
def set_account_uri(self, uri: str) -> None:
"""
Set account URI. For ACME v2, it needs to be used to sending signed
requests.
"""
self.account_uri = uri
if self.account_jws_header:
self.account_jws_header.pop("jwk", None)
self.account_jws_header["kid"] = self.account_uri
def parse_key(
self,
*,
key_file: str | os.PathLike | None = None,
key_content: str | None = None,
passphrase: str | None = None,
) -> dict[str, t.Any]:
"""
Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
In case of an error, raises KeyParsingError.
"""
if key_file is None and key_content is None:
raise AssertionError("One of key_file and key_content must be specified!")
return self.backend.parse_key(
key_file=key_file, key_content=key_content, passphrase=passphrase
)
def sign_request(
self,
*,
protected: dict[str, t.Any],
payload: str | dict[str, t.Any] | None,
key_data: dict[str, t.Any],
encode_payload: bool = True,
) -> dict[str, t.Any]:
"""
Signs an ACME request.
"""
try:
if payload is None:
# POST-as-GET
payload64 = ""
else:
# POST
if encode_payload:
payload = self.module.jsonify(payload).encode("utf8")
payload64 = nopad_b64(to_bytes(payload))
protected64 = nopad_b64(self.module.jsonify(protected).encode("utf8"))
except Exception as e:
raise ModuleFailException(
f"Failed to encode payload / headers as JSON: {e}"
) from e
return self.backend.sign(
payload64=payload64, protected64=protected64, key_data=key_data
)
def _log(self, msg: str, *, data: t.Any = None) -> None:
"""
Write arguments to acme.log when logging is enabled.
"""
if self._debug:
with open("acme.log", "ab") as f:
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%s")
f.write(f"[{timestamp}] {msg}\n".encode("utf-8"))
if data is not None:
f.write(
f"{json.dumps(data, indent=2, sort_keys=True)}\n\n".encode(
"utf-8"
)
)
@t.overload
def send_signed_request(
self,
url: str,
payload: str | dict[str, t.Any] | None,
*,
key_data: dict[str, t.Any] | None = None,
jws_header: dict[str, t.Any] | None = None,
parse_json_result: t.Literal[True] = True,
encode_payload: bool = True,
fail_on_error: bool = True,
error_msg: str | None = None,
expected_status_codes: t.Iterable[int] | None = None,
) -> tuple[dict[str, t.Any], dict[str, t.Any]]: ...
@t.overload
def send_signed_request(
self,
url: str,
payload: str | dict[str, t.Any] | None,
*,
key_data: dict[str, t.Any] | None = None,
jws_header: dict[str, t.Any] | None = None,
parse_json_result: t.Literal[False],
encode_payload: bool = True,
fail_on_error: bool = True,
error_msg: str | None = None,
expected_status_codes: t.Iterable[int] | None = None,
) -> tuple[bytes, dict[str, t.Any]]: ...
def send_signed_request(
self,
url: str,
payload: str | dict[str, t.Any] | None,
*,
key_data: dict[str, t.Any] | None = None,
jws_header: dict[str, t.Any] | None = None,
parse_json_result: bool = True,
encode_payload: bool = True,
fail_on_error: bool = True,
error_msg: str | None = None,
expected_status_codes: t.Iterable[int] | None = None,
) -> tuple[dict[str, t.Any] | bytes, dict[str, t.Any]]:
"""
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
(if parse_json_result is False).
https://tools.ietf.org/html/rfc8555#section-6.2
If payload is None, a POST-as-GET is performed.
(https://tools.ietf.org/html/rfc8555#section-6.3)
"""
key_data = key_data or self.account_key_data
if key_data is None:
raise ModuleFailException("Missing key data")
jws_header = jws_header or self.account_jws_header
if jws_header is None:
raise ModuleFailException("Missing JWS header")
failed_tries = 0
while True:
protected = copy.deepcopy(jws_header)
protected["nonce"] = self.directory.get_nonce()
protected["url"] = url
self._log("URL", data=url)
self._log("protected", data=protected)
self._log("payload", data=payload)
data = self.sign_request(
protected=protected,
payload=payload,
key_data=key_data,
encode_payload=encode_payload,
)
self._log("signed request", data=data)
data = self.module.jsonify(data)
headers = {
"Content-Type": "application/jose+json",
}
resp, info = fetch_url(
self.module,
url,
data=data,
headers=headers,
method="POST",
timeout=self.request_timeout,
)
if _decode_retry(
module=self.module, response=resp, info=info, retry_count=failed_tries
):
failed_tries += 1
continue
_assert_fetch_url_success(module=self.module, response=resp, info=info)
result = {}
try:
# In Python 2, reading from a closed response yields a TypeError.
# In Python 3, read() simply returns ''
if resp.closed:
raise TypeError
content = resp.read()
except (AttributeError, TypeError):
content = info.pop("body", None)
if content or not parse_json_result:
if (
parse_json_result
and info["content-type"].startswith("application/json")
) or 400 <= info["status"] < 600:
try:
decoded_result = self.module.from_json(content.decode("utf8"))
self._log("parsed result", data=decoded_result)
# In case of badNonce error, try again (up to 5 times)
# (https://tools.ietf.org/html/rfc8555#section-6.7)
if all(
(
400 <= info["status"] < 600,
decoded_result.get("type")
== "urn:ietf:params:acme:error:badNonce",
failed_tries <= 5,
)
):
failed_tries += 1
continue
if parse_json_result:
result = decoded_result
else:
result = content
except ValueError as exc:
raise NetworkException(
f"Failed to parse the ACME response: {url} {content}"
) from exc
else:
result = content
if fail_on_error and _is_failed(
info=info, expected_status_codes=expected_status_codes
):
raise ACMEProtocolException(
module=self.module,
msg=error_msg,
info=info,
content=content,
content_json=result if parse_json_result else None,
)
return result, info
@t.overload
def get_request(
self,
uri: str,
*,
parse_json_result: t.Literal[True] = True,
headers: dict[str, str] | None = None,
get_only: bool = False,
fail_on_error: bool = True,
error_msg: str | None = None,
expected_status_codes: t.Iterable[int] | None = None,
) -> tuple[dict[str, t.Any], dict[str, t.Any]]: ...
@t.overload
def get_request(
self,
uri: str,
*,
parse_json_result: t.Literal[False],
headers: dict[str, str] | None = None,
get_only: bool = False,
fail_on_error: bool = True,
error_msg: str | None = None,
expected_status_codes: t.Iterable[int] | None = None,
) -> tuple[bytes, dict[str, t.Any]]: ...
def get_request(
self,
uri: str,
*,
parse_json_result: bool = True,
headers: dict[str, str] | None = None,
get_only: bool = False,
fail_on_error: bool = True,
error_msg: str | None = None,
expected_status_codes: t.Iterable[int] | None = None,
) -> tuple[dict[str, t.Any] | bytes, dict[str, t.Any]]:
"""
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.
"""
if not get_only:
# Try POST-as-GET
content, info = self.send_signed_request(
uri, None, parse_json_result=False, fail_on_error=False
)
if info["status"] == 405:
# Instead, do unauthenticated GET
get_only = True
else:
# Do unauthenticated GET
get_only = True
if get_only:
# Perform unauthenticated GET
retry_count = 0
while True:
resp, info = fetch_url(
self.module,
uri,
method="GET",
headers=headers,
timeout=self.request_timeout,
)
if not _decode_retry(
module=self.module,
response=resp,
info=info,
retry_count=retry_count,
):
break
retry_count += 1
_assert_fetch_url_success(module=self.module, response=resp, info=info)
try:
# In Python 2, reading from a closed response yields a TypeError.
# In Python 3, read() simply returns ''
if resp.closed:
raise TypeError
content = resp.read()
except (AttributeError, TypeError):
content = info.pop("body", None)
# Process result
parsed_json_result = False
result: dict[str, t.Any] | bytes
if parse_json_result:
result = {}
if content:
if info["content-type"].startswith("application/json"):
try:
result = self.module.from_json(content.decode("utf8"))
parsed_json_result = True
except ValueError as exc:
raise NetworkException(
f"Failed to parse the ACME response: {uri} {content!r}"
) from exc
else:
result = content
else:
result = content
if fail_on_error and _is_failed(
info=info, expected_status_codes=expected_status_codes
):
raise ACMEProtocolException(
module=self.module,
msg=error_msg,
info=info,
content=content,
content_json=(
t.cast(dict[str, t.Any], result) if parsed_json_result else None
),
)
return result, info
def get_renewal_info(
self,
*,
cert_id: str | None = None,
cert_info: CertificateInformation | None = None,
cert_filename: str | os.PathLike | None = None,
cert_content: str | bytes | None = None,
include_retry_after: bool = False,
retry_after_relative_with_timezone: bool = True,
) -> dict[str, t.Any]:
if not self.directory.has_renewal_info_endpoint():
raise ModuleFailException(
"The ACME endpoint does not support ACME Renewal Information retrieval"
)
if cert_id is None:
cert_id = compute_cert_id(
backend=self.backend,
cert_info=cert_info,
cert_filename=cert_filename,
cert_content=cert_content,
)
url = f"{self.directory.directory['renewalInfo'].rstrip('/')}/{cert_id}"
data, info = self.get_request(
url, parse_json_result=True, fail_on_error=True, get_only=True
)
# Include Retry-After header if asked for
if include_retry_after and "retry-after" in info:
try:
data["retryAfter"] = parse_retry_after(
info["retry-after"],
relative_with_timezone=retry_after_relative_with_timezone,
)
except ValueError:
pass
return data
def create_default_argspec(
*,
with_account: bool = True,
require_account_key: bool = True,
with_certificate: bool = False,
) -> ArgumentSpec:
"""
Provides default argument spec for the options documented in the acme doc fragment.
"""
result = ArgumentSpec(
argument_spec={
"acme_directory": {"type": "str", "required": True},
"acme_version": {"type": "int", "choices": [2], "default": 2},
"validate_certs": {"type": "bool", "default": True},
"select_crypto_backend": {
"type": "str",
"default": "auto",
"choices": ["auto", "openssl", "cryptography"],
},
"request_timeout": {"type": "int", "default": 10},
},
)
if with_account:
result.update_argspec(
account_key_src={"type": "path", "aliases": ["account_key"]},
account_key_content={"type": "str", "no_log": True},
account_key_passphrase={"type": "str", "no_log": True},
account_uri={"type": "str"},
)
if require_account_key:
result.update(required_one_of=[["account_key_src", "account_key_content"]])
result.update(mutually_exclusive=[["account_key_src", "account_key_content"]])
if with_certificate:
result.update_argspec(
csr={"type": "path"},
csr_content={"type": "str"},
)
result.update(
required_one_of=[["csr", "csr_content"]],
mutually_exclusive=[["csr", "csr_content"]],
)
return result
def create_backend(
module: AnsibleModule, *, needs_acme_v2: bool = True
) -> CryptoBackend:
backend = module.params["select_crypto_backend"]
# Backend autodetect
if backend == "auto":
backend = "cryptography" if HAS_CURRENT_CRYPTOGRAPHY else "openssl"
# Create backend object
module_backend: CryptoBackend
if backend == "cryptography":
if CRYPTOGRAPHY_ERROR is not None:
# Either we could not import cryptography at all, or there was an unexpected error
if CRYPTOGRAPHY_VERSION is None:
msg = missing_required_lib("cryptography")
else:
msg = f"Unexpected error while preparing cryptography: {CRYPTOGRAPHY_ERROR.splitlines()[-1]}"
module.fail_json(msg=msg, exception=CRYPTOGRAPHY_ERROR)
if not HAS_CURRENT_CRYPTOGRAPHY:
# We succeeded importing cryptography, but its version is too old.
mrl = missing_required_lib(
f"cryptography >= {CRYPTOGRAPHY_MINIMAL_VERSION}"
)
module.fail_json(
msg=f"Found cryptography, but only version {CRYPTOGRAPHY_VERSION}. {mrl}"
)
module.debug(
f"Using cryptography backend (library version {CRYPTOGRAPHY_VERSION})"
)
module_backend = CryptographyBackend(module=module)
elif backend == "openssl":
module.debug("Using OpenSSL binary backend")
module_backend = OpenSSLCLIBackend(module=module)
else:
module.fail_json(msg=f'Unknown crypto backend "{backend}"!')
# Check common module parameters
if not module.params["validate_certs"]:
module.warn(
"Disabling certificate validation for communications with ACME endpoint. "
"This should only be done for testing against a local ACME server for "
"development purposes, but *never* for production purposes."
)
# AnsibleModule() changes the locale, so change it back to C because we rely
# on datetime.datetime.strptime() when parsing certificate dates.
locale.setlocale(locale.LC_ALL, "C")
return module_backend
__all__ = (
"ACMEDirectory",
"ACMEClient",
"create_default_argspec",
"create_backend",
)

View File

@@ -0,0 +1,550 @@
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import base64
import binascii
import os
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible_collections.community.crypto.plugins.module_utils._acme.backends import (
CertificateInformation,
CryptoBackend,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.certificates import (
ChainMatcher,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
BackendException,
KeyParsingError,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.io import read_file
from ansible_collections.community.crypto.plugins.module_utils._acme.utils import (
nopad_b64,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_name_to_oid,
get_not_valid_after,
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 (
extract_first_pem,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.support import (
parse_name_field,
)
from ansible_collections.community.crypto.plugins.module_utils._time import (
add_or_remove_timezone,
)
from ansible_collections.community.crypto.plugins.module_utils._version import (
LooseVersion,
)
CRYPTOGRAPHY_MINIMAL_VERSION = "1.5"
CRYPTOGRAPHY_ERROR = None
try:
import cryptography
import cryptography.hazmat.backends
import cryptography.hazmat.primitives.asymmetric.ec
import cryptography.hazmat.primitives.asymmetric.padding
import cryptography.hazmat.primitives.asymmetric.rsa
import cryptography.hazmat.primitives.asymmetric.utils
import cryptography.hazmat.primitives.hashes
import cryptography.hazmat.primitives.hmac
import cryptography.hazmat.primitives.serialization
import cryptography.x509
import cryptography.x509.oid
except ImportError:
HAS_CURRENT_CRYPTOGRAPHY = False
CRYPTOGRAPHY_VERSION = None
CRYPTOGRAPHY_ERROR = traceback.format_exc()
else:
CRYPTOGRAPHY_VERSION = cryptography.__version__
HAS_CURRENT_CRYPTOGRAPHY = LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion(
CRYPTOGRAPHY_MINIMAL_VERSION
)
if t.TYPE_CHECKING:
import datetime
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._acme.certificates import (
CertificateChain,
Criterium,
)
class CryptographyChainMatcher(ChainMatcher):
@staticmethod
def _parse_key_identifier(
*,
key_identifier: str | None,
name: str,
criterium_idx: int,
module: AnsibleModule,
) -> bytes | None:
if key_identifier:
try:
return binascii.unhexlify(key_identifier.replace(":", ""))
except Exception:
if criterium_idx is None:
module.warn(
f"Criterium has invalid {name} value. Ignoring criterium."
)
else:
module.warn(
f"Criterium {criterium_idx} in select_chain has invalid {name} value. "
"Ignoring criterium."
)
return None
def __init__(self, *, criterium: Criterium, module: AnsibleModule) -> None:
self.criterium = criterium
self.test_certificates = criterium.test_certificates
self.subject: list[tuple[cryptography.x509.oid.ObjectIdentifier, str]] = []
self.issuer: list[tuple[cryptography.x509.oid.ObjectIdentifier, str]] = []
if criterium.subject:
self.subject = [
(cryptography_name_to_oid(k), to_text(v))
for k, v in parse_name_field(
criterium.subject, name_field_name="subject"
)
]
if criterium.issuer:
self.issuer = [
(cryptography_name_to_oid(k), to_text(v))
for k, v in parse_name_field(criterium.issuer, name_field_name="issuer")
]
self.subject_key_identifier = CryptographyChainMatcher._parse_key_identifier(
key_identifier=criterium.subject_key_identifier,
name="subject_key_identifier",
criterium_idx=criterium.index,
module=module,
)
self.authority_key_identifier = CryptographyChainMatcher._parse_key_identifier(
key_identifier=criterium.authority_key_identifier,
name="authority_key_identifier",
criterium_idx=criterium.index,
module=module,
)
self.module = module
def _match_subject(
self,
*,
x509_subject: cryptography.x509.Name,
match_subject: list[tuple[cryptography.x509.oid.ObjectIdentifier, str]],
) -> bool:
for oid, value in match_subject:
found = False
for attribute in x509_subject:
if attribute.oid == oid and value == to_text(attribute.value):
found = True
break
if not found:
return False
return True
def match(self, *, certificate: CertificateChain) -> bool:
"""
Check whether an alternate chain matches the specified criterium.
"""
chain = certificate.chain
if self.test_certificates == "last":
chain = chain[-1:]
elif self.test_certificates == "first":
chain = chain[:1]
for cert in chain:
try:
x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert))
matches = True
if not self._match_subject(
x509_subject=x509.subject, match_subject=self.subject
):
matches = False
if not self._match_subject(
x509_subject=x509.issuer, match_subject=self.issuer
):
matches = False
if self.subject_key_identifier:
try:
ext_ski = x509.extensions.get_extension_for_class(
cryptography.x509.SubjectKeyIdentifier
)
if self.subject_key_identifier != ext_ski.value.digest:
matches = False
except cryptography.x509.ExtensionNotFound:
matches = False
if self.authority_key_identifier:
try:
ext_aki = x509.extensions.get_extension_for_class(
cryptography.x509.AuthorityKeyIdentifier
)
if (
self.authority_key_identifier
!= ext_aki.value.key_identifier
):
matches = False
except cryptography.x509.ExtensionNotFound:
matches = False
if matches:
return True
except Exception as e:
self.module.warn(f"Error while loading certificate {cert}: {e}")
return False
class CryptographyBackend(CryptoBackend):
def __init__(self, *, module: AnsibleModule) -> None:
super().__init__(module=module, with_timezone=CRYPTOGRAPHY_TIMEZONE)
def parse_key(
self,
*,
key_file: str | os.PathLike | None = None,
key_content: str | None = None,
passphrase: str | None = None,
) -> dict[str, t.Any]:
"""
Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
Raises KeyParsingError in case of errors.
"""
# If key_content is not given, read key_file
if key_content is None:
if key_file is None:
raise KeyParsingError(
"one of key_file and key_content must be specified"
)
b_key_content = read_file(key_file)
else:
b_key_content = to_bytes(key_content)
# Parse key
try:
key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
b_key_content,
password=to_bytes(passphrase) if passphrase is not None else None,
)
except Exception as e:
raise KeyParsingError(f"error while loading key: {e}") from e
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
rsa_pk = key.public_key().public_numbers()
return {
"key_obj": key,
"type": "rsa",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": nopad_b64(convert_int_to_bytes(rsa_pk.e)),
"n": nopad_b64(convert_int_to_bytes(rsa_pk.n)),
},
"hash": "sha256",
}
if isinstance(
key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey
):
ec_pk = key.public_key().public_numbers()
if ec_pk.curve.name == "secp256r1":
bits = 256
alg = "ES256"
hashalg = "sha256"
point_size = 32
curve = "P-256"
elif ec_pk.curve.name == "secp384r1":
bits = 384
alg = "ES384"
hashalg = "sha384"
point_size = 48
curve = "P-384"
elif ec_pk.curve.name == "secp521r1":
# Not yet supported on Let's Encrypt side, see
# https://github.com/letsencrypt/boulder/issues/2217
bits = 521
alg = "ES512"
hashalg = "sha512"
point_size = 66
curve = "P-521"
else:
raise KeyParsingError(f"unknown elliptic curve: {ec_pk.curve.name}")
num_bytes = (bits + 7) // 8
return {
"key_obj": key,
"type": "ec",
"alg": alg,
"jwk": {
"kty": "EC",
"crv": curve,
"x": nopad_b64(convert_int_to_bytes(ec_pk.x, count=num_bytes)),
"y": nopad_b64(convert_int_to_bytes(ec_pk.y, count=num_bytes)),
},
"hash": hashalg,
"point_size": point_size,
}
raise KeyParsingError(f'unknown key type "{type(key)}"')
def sign(
self, *, payload64: str, protected64: str, key_data: dict[str, t.Any]
) -> dict[str, t.Any]:
sign_payload = f"{protected64}.{payload64}".encode("utf8")
hashalg: type[cryptography.hazmat.primitives.hashes.HashAlgorithm]
if "mac_obj" in key_data:
mac = key_data["mac_obj"]()
mac.update(sign_payload)
signature = mac.finalize()
elif isinstance(
key_data["key_obj"],
cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey,
):
padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15()
hashalg = cryptography.hazmat.primitives.hashes.SHA256
signature = key_data["key_obj"].sign(sign_payload, padding, hashalg())
elif isinstance(
key_data["key_obj"],
cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey,
):
if key_data["hash"] == "sha256":
hashalg = cryptography.hazmat.primitives.hashes.SHA256
elif key_data["hash"] == "sha384":
hashalg = cryptography.hazmat.primitives.hashes.SHA384
elif key_data["hash"] == "sha512":
hashalg = cryptography.hazmat.primitives.hashes.SHA512
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)
)
rr = convert_int_to_hex(r, digits=2 * key_data["point_size"])
ss = convert_int_to_hex(s, digits=2 * key_data["point_size"])
signature = binascii.unhexlify(rr) + binascii.unhexlify(ss)
else:
raise AssertionError("Can never be reached") # pragma: no cover
return {
"protected": protected64,
"payload": payload64,
"signature": nopad_b64(signature),
}
def create_mac_key(self, *, alg: str, key: str) -> dict[str, t.Any]:
"""Create a MAC key."""
hashalg: type[cryptography.hazmat.primitives.hashes.HashAlgorithm]
if alg == "HS256":
hashalg = cryptography.hazmat.primitives.hashes.SHA256
hashbytes = 32
elif alg == "HS384":
hashalg = cryptography.hazmat.primitives.hashes.SHA384
hashbytes = 48
elif alg == "HS512":
hashalg = cryptography.hazmat.primitives.hashes.SHA512
hashbytes = 64
else:
raise BackendException(
f"Unsupported MAC key algorithm for cryptography backend: {alg}"
)
key_bytes = base64.urlsafe_b64decode(key)
if len(key_bytes) < hashbytes:
raise BackendException(
f"{alg} key must be at least {hashbytes} bytes long (after Base64 decoding)"
)
return {
"mac_obj": lambda: cryptography.hazmat.primitives.hmac.HMAC(
key_bytes, hashalg()
),
"type": "hmac",
"alg": alg,
"jwk": {
"kty": "oct",
"k": key,
},
}
def get_ordered_csr_identifiers(
self,
*,
csr_filename: str | os.PathLike | None = None,
csr_content: str | bytes | None = None,
) -> list[tuple[str, str]]:
"""
Return a list of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
The list is deduplicated, and if a CNAME is present, it will be returned
as the first element in the result.
"""
if csr_content is None:
if csr_filename is None:
raise BackendException(
"One of csr_content and csr_filename has to be provided"
)
b_csr_content = read_file(csr_filename)
else:
b_csr_content = to_bytes(csr_content)
csr = cryptography.x509.load_pem_x509_csr(b_csr_content)
identifiers = set()
result = []
def add_identifier(identifier: tuple[str, str]) -> None:
if identifier in identifiers:
return
identifiers.add(identifier)
result.append(identifier)
for sub in csr.subject:
if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME:
add_identifier(("dns", t.cast(str, sub.value)))
for extension in csr.extensions:
if (
extension.oid
== cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
):
for name in extension.value:
if isinstance(name, cryptography.x509.DNSName):
add_identifier(("dns", name.value))
elif isinstance(name, cryptography.x509.IPAddress):
add_identifier(("ip", name.value.compressed))
else:
raise BackendException(
f"Found unsupported SAN identifier {name}"
)
return result
def get_csr_identifiers(
self,
*,
csr_filename: str | os.PathLike | None = None,
csr_content: str | bytes | bytes | None = None,
) -> set[tuple[str, str]]:
"""
Return a set of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
"""
return set(
self.get_ordered_csr_identifiers(
csr_filename=csr_filename, csr_content=csr_content
)
)
def get_cert_days(
self,
*,
cert_filename: str | os.PathLike | None = None,
cert_content: str | bytes | None = None,
now: datetime.datetime | None = None,
) -> int:
"""
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
certificate, only the first one will be considered.
If now is not specified, datetime.datetime.now() is used.
"""
if cert_filename is not None:
cert_content = None
if os.path.exists(cert_filename):
cert_content = read_file(cert_filename)
else:
cert_content = to_bytes(cert_content)
if cert_content is None:
return -1
# Make sure we have at most one PEM. Otherwise cryptography 36.0.0 will barf.
b_cert_content = to_bytes(extract_first_pem(to_text(cert_content)) or "")
try:
cert = cryptography.x509.load_pem_x509_certificate(b_cert_content)
except Exception as e:
if cert_filename is None:
raise BackendException(f"Cannot parse certificate: {e}") from e
raise BackendException(
f"Cannot parse certificate {cert_filename}: {e}"
) from e
if now is None:
now = self.get_now()
else:
now = add_or_remove_timezone(now, with_timezone=CRYPTOGRAPHY_TIMEZONE)
return (get_not_valid_after(cert) - now).days
def create_chain_matcher(self, *, criterium: Criterium) -> ChainMatcher:
"""
Given a Criterium object, creates a ChainMatcher object.
"""
return CryptographyChainMatcher(criterium=criterium, module=self.module)
def get_cert_information(
self,
*,
cert_filename: str | os.PathLike | None = None,
cert_content: str | bytes | None = None,
) -> CertificateInformation:
"""
Return some information on a X.509 certificate as a CertificateInformation object.
"""
if cert_filename is not None:
cert_content = read_file(cert_filename)
else:
cert_content = to_bytes(cert_content)
# Make sure we have at most one PEM. Otherwise cryptography 36.0.0 will barf.
b_cert_content = to_bytes(extract_first_pem(to_text(cert_content)) or "")
try:
cert = cryptography.x509.load_pem_x509_certificate(b_cert_content)
except Exception as e:
if cert_filename is None:
raise BackendException(f"Cannot parse certificate: {e}") from e
raise BackendException(
f"Cannot parse certificate {cert_filename}: {e}"
) from e
ski = None
try:
ext_ski = cert.extensions.get_extension_for_class(
cryptography.x509.SubjectKeyIdentifier
)
ski = ext_ski.value.digest
except cryptography.x509.ExtensionNotFound:
pass
aki = None
try:
ext_aki = cert.extensions.get_extension_for_class(
cryptography.x509.AuthorityKeyIdentifier
)
aki = ext_aki.value.key_identifier
except cryptography.x509.ExtensionNotFound:
pass
return CertificateInformation(
not_valid_after=get_not_valid_after(cert),
not_valid_before=get_not_valid_before(cert),
serial_number=cert.serial_number,
subject_key_identifier=ski,
authority_key_identifier=aki,
)
__all__ = (
"CRYPTOGRAPHY_MINIMAL_VERSION",
"CRYPTOGRAPHY_ERROR",
"CRYPTOGRAPHY_VERSION",
"CRYPTOGRAPHY_ERROR",
"CryptographyBackend",
)

View File

@@ -0,0 +1,625 @@
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import base64
import binascii
import datetime
import ipaddress
import os
import re
import tempfile
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible_collections.community.crypto.plugins.module_utils._acme.backends import (
CertificateInformation,
CryptoBackend,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
BackendException,
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,
)
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._acme.certificates import (
Criterium,
)
_OPENSSL_ENVIRONMENT_UPDATE = {
"LANG": "C",
"LC_ALL": "C",
"LC_MESSAGES": "C",
"LC_CTYPE": "C",
}
def _extract_date(
out_text: str, *, name: str, cert_filename_suffix: str = ""
) -> datetime.datetime:
matcher = re.search(rf"\s+{name}\s*:\s+(.*)", out_text)
if matcher is None:
raise BackendException(f"No '{name}' date found{cert_filename_suffix}")
date_str = matcher.group(1)
try:
# For some reason Python's strptime() does not return any timezone information,
# 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
# replacing it by UTC.
return ensure_utc_timezone(
datetime.datetime.strptime(date_str, "%b %d %H:%M:%S %Y %Z")
)
except ValueError as exc:
raise BackendException(
f"Failed to parse '{name}' date{cert_filename_suffix}: {exc}"
) from exc
def _decode_octets(octets_text: str) -> bytes:
return binascii.unhexlify(re.sub(r"(\s|:)", "", octets_text).encode("utf-8"))
@t.overload
def _extract_octets(
out_text: str,
*,
name: str,
required: t.Literal[False],
potential_prefixes: t.Iterable[str] | None = None,
) -> bytes | None: ...
@t.overload
def _extract_octets(
out_text: str,
*,
name: str,
required: t.Literal[True],
potential_prefixes: t.Iterable[str] | None = None,
) -> bytes: ...
def _extract_octets(
out_text: str,
*,
name: str,
required: bool = True,
potential_prefixes: t.Iterable[str] | None = None,
) -> bytes | None:
part = (
f"(?:{'|'.join(re.escape(pp) for pp in potential_prefixes)})"
if potential_prefixes
else ""
)
regexp = rf"\s+{name}:\s*\n\s+{part}([A-Fa-f0-9]{{2}}(?::[A-Fa-f0-9]{{2}})*)\s*\n"
match = re.search(regexp, out_text, re.MULTILINE | re.DOTALL)
if match is not None:
return _decode_octets(match.group(1))
if not required:
return None
raise BackendException(f"No '{name}' octet string found")
class OpenSSLCLIBackend(CryptoBackend):
def __init__(
self, *, module: AnsibleModule, openssl_binary: str | None = None
) -> None:
super().__init__(module=module, with_timezone=True)
if openssl_binary is None:
openssl_binary = module.get_bin_path("openssl", True)
self.openssl_binary = openssl_binary
def parse_key(
self,
*,
key_file: str | os.PathLike | None = None,
key_content: str | None = None,
passphrase: str | None = None,
) -> dict[str, t.Any]:
"""
Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
Raises KeyParsingError in case of errors.
"""
if passphrase is not None:
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 None:
if key_content is None:
raise KeyParsingError(
"one of key_file and key_content must be specified"
)
fd, tmpsrc = tempfile.mkstemp()
self.module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
f = os.fdopen(fd, "wb")
try:
f.write(key_content.encode("utf-8"))
key_file = tmpsrc
except Exception as err:
try:
f.close()
except Exception:
pass
raise KeyParsingError(
f"failed to create temporary content file: {err}",
exception=traceback.format_exc(),
) from err
f.close()
# Parse key
account_key_type = None
with open(key_file, "r", encoding="utf-8") as fi:
for line in fi:
m = re.match(
r"^\s*-{5,}BEGIN\s+(EC|RSA)\s+PRIVATE\s+KEY-{5,}\s*$", line
)
if m is not None:
account_key_type = m.group(1).lower()
break
if account_key_type is None:
# This happens for example if openssl_privatekey created this key
# (as opposed to the OpenSSL binary). For now, we assume this is
# an RSA key.
# FIXME: add some kind of auto-detection
account_key_type = "rsa"
if account_key_type not in ("rsa", "ec"):
raise KeyParsingError(f'unknown key type "{account_key_type}"')
openssl_keydump_cmd = [
self.openssl_binary,
account_key_type,
"-in",
str(key_file),
"-noout",
"-text",
]
rc, out, stderr = self.module.run_command(
openssl_keydump_cmd,
check_rc=False,
environ_update=_OPENSSL_ENVIRONMENT_UPDATE,
)
if rc != 0:
raise BackendException(
f"Error while running {' '.join(openssl_keydump_cmd)}: {stderr}"
)
out_text = to_text(out, errors="surrogate_or_strict")
if account_key_type == "rsa":
matcher = re.search(
r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent",
out_text,
re.MULTILINE | re.DOTALL,
)
if matcher is None:
raise KeyParsingError("cannot parse RSA key: modulus not found")
pub_hex = matcher.group(1)
matcher = re.search(
r"\npublicExponent: ([0-9]+)", out_text, re.MULTILINE | re.DOTALL
)
if matcher is None:
raise KeyParsingError("cannot parse RSA key: public exponent not found")
pub_exp = matcher.group(1)
pub_exp = f"{int(pub_exp):x}"
if len(pub_exp) % 2:
pub_exp = f"0{pub_exp}"
return {
"key_file": str(key_file),
"type": "rsa",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": nopad_b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
"n": nopad_b64(_decode_octets(pub_hex)),
},
"hash": "sha256",
}
if account_key_type == "ec":
pub_data = re.search(
r"pub:\s*\n\s+04:([a-f0-9\:\s]+?)\nASN1 OID: (\S+)(?:\nNIST CURVE: (\S+))?",
out_text,
re.MULTILINE | re.DOTALL,
)
if pub_data is None:
raise KeyParsingError("cannot parse elliptic curve key")
pub_hex = _decode_octets(pub_data.group(1))
asn1_oid_curve = pub_data.group(2).lower()
nist_curve = pub_data.group(3).lower() if pub_data.group(3) else None
if asn1_oid_curve == "prime256v1" or nist_curve == "p-256":
bits = 256
alg = "ES256"
hashalg = "sha256"
point_size = 32
curve = "P-256"
elif asn1_oid_curve == "secp384r1" or nist_curve == "p-384":
bits = 384
alg = "ES384"
hashalg = "sha384"
point_size = 48
curve = "P-384"
elif asn1_oid_curve == "secp521r1" or nist_curve == "p-521":
# Not yet supported on Let's Encrypt side, see
# https://github.com/letsencrypt/boulder/issues/2217
bits = 521
alg = "ES512"
hashalg = "sha512"
point_size = 66
curve = "P-521"
else:
raise KeyParsingError(
f"unknown elliptic curve: {asn1_oid_curve} / {nist_curve}"
)
num_bytes = (bits + 7) // 8
if len(pub_hex) != 2 * num_bytes:
raise KeyParsingError(
f"bad elliptic curve point ({asn1_oid_curve} / {nist_curve})"
)
return {
"key_file": key_file,
"type": "ec",
"alg": alg,
"jwk": {
"kty": "EC",
"crv": curve,
"x": nopad_b64(pub_hex[:num_bytes]),
"y": nopad_b64(pub_hex[num_bytes:]),
},
"hash": hashalg,
"point_size": point_size,
}
raise KeyParsingError(
f"Internal error: unexpected account_key_type = {account_key_type!r}"
)
def sign(
self, *, payload64: str, protected64: str, key_data: dict[str, t.Any]
) -> dict[str, t.Any]:
sign_payload = f"{protected64}.{payload64}".encode("utf8")
if key_data["type"] == "hmac":
hex_key = (
binascii.hexlify(base64.urlsafe_b64decode(key_data["jwk"]["k"]))
).decode("ascii")
cmd_postfix = [
"-mac",
"hmac",
"-macopt",
f"hexkey:{hex_key}",
"-binary",
]
else:
cmd_postfix = ["-sign", key_data["key_file"]]
openssl_sign_cmd = [
self.openssl_binary,
"dgst",
f"-{key_data['hash']}",
] + cmd_postfix
rc, out, err = self.module.run_command(
openssl_sign_cmd,
data=sign_payload,
check_rc=False,
binary_data=True,
environ_update=_OPENSSL_ENVIRONMENT_UPDATE,
)
if rc != 0:
raise BackendException(
f"Error while running {' '.join(openssl_sign_cmd)}: {err}"
)
if key_data["type"] == "ec":
dummy, der_out, dummy = self.module.run_command(
[self.openssl_binary, "asn1parse", "-inform", "DER"],
data=out,
binary_data=True,
environ_update=_OPENSSL_ENVIRONMENT_UPDATE,
)
expected_len = 2 * key_data["point_size"]
sig = re.findall(
rf"prim:\s+INTEGER\s+:([0-9A-F]{{1,{expected_len}}})\n",
to_text(der_out, errors="surrogate_or_strict"),
)
if len(sig) != 2:
der_output = to_text(der_out, errors="surrogate_or_strict")
raise BackendException(
f"failed to generate Elliptic Curve signature; cannot parse DER output: {der_output}"
)
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])
return {
"protected": protected64,
"payload": payload64,
"signature": nopad_b64(to_bytes(out)),
}
def create_mac_key(self, *, alg: str, key: str) -> dict[str, t.Any]:
"""Create a MAC key."""
if alg == "HS256":
hashalg = "sha256"
hashbytes = 32
elif alg == "HS384":
hashalg = "sha384"
hashbytes = 48
elif alg == "HS512":
hashalg = "sha512"
hashbytes = 64
else:
raise BackendException(
f"Unsupported MAC key algorithm for OpenSSL backend: {alg}"
)
key_bytes = base64.urlsafe_b64decode(key)
if len(key_bytes) < hashbytes:
raise BackendException(
f"{alg} key must be at least {hashbytes} bytes long (after Base64 decoding)"
)
return {
"type": "hmac",
"alg": alg,
"jwk": {
"kty": "oct",
"k": key,
},
"hash": hashalg,
}
@staticmethod
def _normalize_ip(ip: str) -> str:
try:
return ipaddress.ip_address(ip).compressed
except ValueError:
# We do not want to error out on something IPAddress() cannot parse
return ip
def get_ordered_csr_identifiers(
self,
*,
csr_filename: str | os.PathLike | None = None,
csr_content: str | bytes | None = None,
) -> list[tuple[str, str]]:
"""
Return a list of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
The list is deduplicated, and if a CNAME is present, it will be returned
as the first element in the result.
"""
filename = csr_filename
data = None
if csr_content is not None:
filename = "/dev/stdin"
data = to_bytes(csr_content)
openssl_csr_cmd = [
self.openssl_binary,
"req",
"-in",
str(filename),
"-noout",
"-text",
]
rc, out, err = self.module.run_command(
openssl_csr_cmd,
data=data,
check_rc=False,
binary_data=True,
environ_update=_OPENSSL_ENVIRONMENT_UPDATE,
)
if rc != 0:
raise BackendException(
f"Error while running {' '.join(openssl_csr_cmd)}: {err}"
)
identifiers = set()
result = []
def add_identifier(identifier: tuple[str, str]) -> None:
if identifier in identifiers:
return
identifiers.add(identifier)
result.append(identifier)
common_name = re.search(
r"Subject:.* CN\s?=\s?([^\s,;/]+)",
to_text(out, errors="surrogate_or_strict"),
)
if common_name is not None:
add_identifier(("dns", common_name.group(1)))
subject_alt_names = re.search(
r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n",
to_text(out, errors="surrogate_or_strict"),
re.MULTILINE | re.DOTALL,
)
if subject_alt_names is not None:
for san in subject_alt_names.group(1).split(", "):
if san.lower().startswith("dns:"):
add_identifier(("dns", san[4:]))
elif san.lower().startswith("ip:"):
add_identifier(("ip", self._normalize_ip(san[3:])))
elif san.lower().startswith("ip address:"):
add_identifier(("ip", self._normalize_ip(san[11:])))
else:
raise BackendException(f'Found unsupported SAN identifier "{san}"')
return result
def get_csr_identifiers(
self,
*,
csr_filename: str | os.PathLike | None = None,
csr_content: str | bytes | None = None,
) -> set[tuple[str, str]]:
"""
Return a set of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
"""
return set(
self.get_ordered_csr_identifiers(
csr_filename=csr_filename, csr_content=csr_content
)
)
def get_cert_days(
self,
*,
cert_filename: str | os.PathLike | None = None,
cert_content: str | bytes | None = None,
now: datetime.datetime | None = None,
) -> int:
"""
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
certificate, only the first one will be considered.
If now is not specified, datetime.datetime.now() is used.
"""
filename = cert_filename
data = None
if cert_content is not None:
filename = "/dev/stdin"
data = to_bytes(cert_content)
cert_filename_suffix = ""
elif cert_filename is not None:
if not os.path.exists(cert_filename):
return -1
cert_filename_suffix = f" in {cert_filename}"
else:
return -1
openssl_cert_cmd = [
self.openssl_binary,
"x509",
"-in",
str(filename),
"-noout",
"-text",
]
rc, out, err = self.module.run_command(
openssl_cert_cmd,
data=data,
check_rc=False,
binary_data=True,
environ_update=_OPENSSL_ENVIRONMENT_UPDATE,
)
if rc != 0:
raise BackendException(
f"Error while running {' '.join(openssl_cert_cmd)}: {err}"
)
out_text = to_text(out, errors="surrogate_or_strict")
not_after = _extract_date(
out_text, name="Not After", cert_filename_suffix=cert_filename_suffix
)
if now is None:
now = self.get_now()
else:
now = ensure_utc_timezone(now)
return (not_after - now).days
def create_chain_matcher(self, *, criterium: Criterium) -> t.NoReturn:
"""
Given a Criterium object, creates a ChainMatcher object.
"""
raise BackendException(
'Alternate chain matching can only be used with the "cryptography" backend.'
)
def get_cert_information(
self,
*,
cert_filename: str | os.PathLike | None = None,
cert_content: str | bytes | None = None,
) -> CertificateInformation:
"""
Return some information on a X.509 certificate as a CertificateInformation object.
"""
filename = cert_filename
data = None
if cert_filename is not None:
cert_filename_suffix = f" in {cert_filename}"
else:
filename = "/dev/stdin"
data = to_bytes(cert_content)
cert_filename_suffix = ""
openssl_cert_cmd = [
self.openssl_binary,
"x509",
"-in",
str(filename),
"-noout",
"-text",
]
rc, out, err = self.module.run_command(
openssl_cert_cmd,
data=data,
check_rc=False,
binary_data=True,
environ_update=_OPENSSL_ENVIRONMENT_UPDATE,
)
if rc != 0:
raise BackendException(
f"Error while running {' '.join(openssl_cert_cmd)}: {err}"
)
out_text = to_text(out, errors="surrogate_or_strict")
not_after = _extract_date(
out_text, name="Not After", cert_filename_suffix=cert_filename_suffix
)
not_before = _extract_date(
out_text, name="Not Before", cert_filename_suffix=cert_filename_suffix
)
sn = re.search(
r" Serial Number: ([0-9]+)",
to_text(out, errors="surrogate_or_strict"),
re.MULTILINE | re.DOTALL,
)
if sn:
serial = int(sn.group(1))
else:
serial = convert_bytes_to_int(
_extract_octets(out_text, name="Serial Number", required=True)
)
ski = _extract_octets(
out_text, name="X509v3 Subject Key Identifier", required=False
)
aki = _extract_octets(
out_text,
name="X509v3 Authority Key Identifier",
required=False,
potential_prefixes=["keyid:", ""],
)
return CertificateInformation(
not_valid_after=not_after,
not_valid_before=not_before,
serial_number=serial,
subject_key_identifier=ski,
authority_key_identifier=aki,
)
__all__ = ("OpenSSLCLIBackend",)

View File

@@ -0,0 +1,230 @@
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import abc
import datetime
import re
import typing as t
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
BackendException,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils._time import (
UTC,
ensure_utc_timezone,
from_epoch_seconds,
get_epoch_seconds,
get_now_datetime,
get_relative_time_option,
remove_timezone,
)
if t.TYPE_CHECKING:
import os
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._acme.certificates import (
ChainMatcher,
Criterium,
)
class CertificateInformation(t.NamedTuple):
not_valid_after: datetime.datetime
not_valid_before: datetime.datetime
serial_number: int
subject_key_identifier: bytes | None
authority_key_identifier: bytes | None
_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: str) -> str:
"""
Given a RFC 3339 timestamp that includes too many digits for the fractional seconds part, reduces these to at most 6.
"""
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
m = _FRACTIONAL_MATCHER.match(timestamp_str)
if not m:
raise BackendException(f"Cannot parse ISO 8601 timestamp {timestamp_str!r}")
timestamp, fractional, timezone = m.groups()
if len(fractional) > 7:
# Python does not support anything smaller than microseconds
# (Golang supports nanoseconds, Boulder often emits more fractional digits, which Python chokes on)
fractional = fractional[:7]
return f"{timestamp}{fractional}{timezone}"
def _parse_acme_timestamp(
timestamp_str: str, *, with_timezone: bool
) -> datetime.datetime:
"""
Parses a RFC 3339 timestamp.
"""
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
timestamp_str = _reduce_fractional_digits(timestamp_str)
for time_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",
):
try:
result = datetime.datetime.strptime(timestamp_str, time_format)
except ValueError:
pass
else:
return (
ensure_utc_timezone(result)
if with_timezone
else remove_timezone(result)
)
raise BackendException(f"Cannot parse ISO 8601 timestamp {timestamp_str!r}")
class CryptoBackend(metaclass=abc.ABCMeta):
def __init__(self, *, module: AnsibleModule, with_timezone: bool = False) -> None:
self.module = module
self._with_timezone = with_timezone
def get_now(self) -> datetime.datetime:
return get_now_datetime(with_timezone=self._with_timezone)
def parse_acme_timestamp(self, timestamp_str: str) -> datetime.datetime:
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
return _parse_acme_timestamp(timestamp_str, with_timezone=self._with_timezone)
def parse_module_parameter(self, *, value: str, name: str) -> datetime.datetime:
try:
result = get_relative_time_option(
value, input_name=name, with_timezone=self._with_timezone
)
if result is None:
raise BackendException(f"Invalid value for {name}: {value!r}")
return result
except OpenSSLObjectError as exc:
raise BackendException(str(exc)) from exc
def interpolate_timestamp(
self,
timestamp_start: datetime.datetime,
timestamp_end: datetime.datetime,
*,
percentage: float,
) -> datetime.datetime:
start = get_epoch_seconds(timestamp_start)
end = get_epoch_seconds(timestamp_end)
return from_epoch_seconds(
start + percentage * (end - start), with_timezone=self._with_timezone
)
def get_utc_datetime(self, *args, **kwargs) -> datetime.datetime:
kwargs_ext: dict[str, t.Any] = dict(kwargs)
if self._with_timezone and ("tzinfo" not in kwargs_ext and len(args) < 8):
kwargs_ext["tzinfo"] = UTC
result = datetime.datetime(*args, **kwargs_ext)
if self._with_timezone and ("tzinfo" in kwargs or len(args) >= 8):
result = ensure_utc_timezone(result)
return result
@abc.abstractmethod
def parse_key(
self,
*,
key_file: str | os.PathLike | None = None,
key_content: str | None = None,
passphrase: str | None = None,
) -> dict[str, t.Any]:
"""
Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
Raises KeyParsingError in case of errors.
"""
@abc.abstractmethod
def sign(
self, *, payload64: str, protected64: str, key_data: dict[str, t.Any]
) -> dict[str, t.Any]:
pass
@abc.abstractmethod
def create_mac_key(self, *, alg: str, key: str) -> dict[str, t.Any]:
"""Create a MAC key."""
@abc.abstractmethod
def get_ordered_csr_identifiers(
self,
*,
csr_filename: str | os.PathLike | None = None,
csr_content: str | bytes | None = None,
) -> list[tuple[str, str]]:
"""
Return a list of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
The list is deduplicated, and if a CNAME is present, it will be returned
as the first element in the result.
"""
@abc.abstractmethod
def get_csr_identifiers(
self,
*,
csr_filename: str | os.PathLike | None = None,
csr_content: str | bytes | None = None,
) -> set[tuple[str, str]]:
"""
Return a set of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
"""
@abc.abstractmethod
def get_cert_days(
self,
*,
cert_filename: str | os.PathLike | None = None,
cert_content: str | bytes | None = None,
now: datetime.datetime | None = None,
) -> int:
"""
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
certificate, only the first one will be considered.
If now is not specified, datetime.datetime.now() is used.
"""
@abc.abstractmethod
def create_chain_matcher(self, *, criterium: Criterium) -> ChainMatcher:
"""
Given a Criterium object, creates a ChainMatcher object.
"""
@abc.abstractmethod
def get_cert_information(
self,
*,
cert_filename: str | os.PathLike | None = None,
cert_content: str | bytes | None = None,
) -> CertificateInformation:
"""
Return some information on a X.509 certificate as a CertificateInformation object.
"""
__all__ = ("CertificateInformation", "CryptoBackend")

View File

@@ -0,0 +1,419 @@
# Copyright (c) 2024 Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import os
import typing as t
from ansible_collections.community.crypto.plugins.module_utils._acme.account import (
ACMEAccount,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.acme import (
ACMEClient,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.certificates import (
CertificateChain,
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 (
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 Order
from ansible_collections.community.crypto.plugins.module_utils._acme.utils import (
pem_to_der,
)
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._acme.backends import (
CryptoBackend,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.certificates import (
ChainMatcher,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.challenges import (
Challenge,
)
class ACMECertificateClient:
"""
ACME v2 client class. Uses an ACME account object and a CSR to
start and validate ACME challenges and download the respective
certificates.
"""
def __init__(
self,
*,
module: AnsibleModule,
backend: CryptoBackend,
client: ACMEClient | None = None,
account: ACMEAccount | None = None,
) -> None:
self.module = module
self.version = module.params["acme_version"]
self.csr = module.params.get("csr")
self.csr_content = module.params.get("csr_content")
if client is None:
client = ACMEClient(module=module, backend=backend)
self.client = client
if account is None:
account = ACMEAccount(client=self.client)
self.account = account
self.order_uri = module.params.get("order_uri")
self.order_creation_error_strategy = module.params.get(
"order_creation_error_strategy", "auto"
)
self.order_creation_max_retries = module.params.get(
"order_creation_max_retries", 3
)
# Make sure account exists
dummy, account_data = self.account.setup_account(allow_creation=False)
if account_data is None:
raise ModuleFailException(msg="Account does not exist or is deactivated.")
if self.csr is not None and not os.path.exists(self.csr):
raise ModuleFailException(f"CSR {self.csr} not found")
# Extract list of identifiers from CSR
if self.csr is not None or self.csr_content is not None:
self.identifiers: list[tuple[str, str]] | None = (
self.client.backend.get_ordered_csr_identifiers(
csr_filename=self.csr, csr_content=self.csr_content
)
)
else:
self.identifiers = None
def parse_select_chain(
self, select_chain: list[dict[str, t.Any]] | None
) -> list[ChainMatcher]:
select_chain_matcher = []
if select_chain:
for criterium_idx, criterium in enumerate(select_chain):
try:
select_chain_matcher.append(
self.client.backend.create_chain_matcher(
criterium=Criterium(
criterium=criterium, index=criterium_idx
)
)
)
except ValueError as exc:
self.module.warn(
f"Error while parsing criterium: {exc}. Ignoring criterium."
)
return select_chain_matcher
def load_order(self) -> Order:
if not self.order_uri:
raise ModuleFailException("The order URI has not been provided")
order = Order.from_url(client=self.client, url=self.order_uri)
order.load_authorizations(client=self.client)
return order
def create_order(
self, *, replaces_cert_id: str | None = None, profile: str | None = None
) -> Order:
"""
Create a new order.
"""
if self.identifiers is None:
raise ModuleFailException("No identifiers have been provided")
order = Order.create_with_error_handling(
client=self.client,
identifiers=self.identifiers,
error_strategy=self.order_creation_error_strategy,
error_max_retries=self.order_creation_max_retries,
replaces_cert_id=replaces_cert_id,
profile=profile,
message_callback=self.module.warn,
)
self.order_uri = order.url
order.load_authorizations(client=self.client)
return order
def get_challenges_data(
self, order: Order
) -> tuple[list[dict[str, t.Any]], dict[str, list[str]]]:
"""
Get challenge details.
Return a tuple of generic challenge details, and specialized DNS challenge details.
"""
data: list[dict[str, t.Any]] = []
data_dns: dict[str, list[str]] = {}
dns_challenge_type = "dns-01"
for authz in order.authorizations.values():
# Skip valid authentications: their challenges are already valid
# and do not need to be returned
if authz.status == "valid":
continue
challenge_data = authz.get_challenge_data(client=self.client)
data.append(
{
"identifier": authz.identifier,
"identifier_type": authz.identifier_type,
"challenges": challenge_data,
}
)
dns_challenge = challenge_data.get(dns_challenge_type)
if dns_challenge:
values = data_dns.get(dns_challenge["record"])
if values is None:
values = []
data_dns[dns_challenge["record"]] = values
values.append(dns_challenge["resource_value"])
return data, data_dns
def check_that_authorizations_can_be_used(self, order: Order) -> None:
bad_authzs = []
for authz in order.authorizations.values():
if authz.status not in ("valid", "pending"):
bad_authzs.append(
f"{authz.combined_identifier} (status={authz.status!r})"
)
if bad_authzs:
bad_authzs_str = ", ".join(sorted(bad_authzs))
raise ModuleFailException(
"Some of the authorizations for the order are in a bad state, so the order"
f" can no longer be satisfied: {bad_authzs_str}",
)
def collect_invalid_authzs(self, order: Order) -> list[Authorization]:
return [
authz
for authz in order.authorizations.values()
if authz.status == "invalid"
]
def collect_pending_authzs(self, order: Order) -> list[Authorization]:
return [
authz
for authz in order.authorizations.values()
if authz.status == "pending"
]
def call_validate(
self,
pending_authzs: list[Authorization],
*,
get_challenge: t.Callable[[Authorization], str],
wait: bool = True,
) -> list[tuple[Authorization, str, Challenge | None]]:
authzs_with_challenges_to_wait_for = []
for authz in pending_authzs:
challenge_type = get_challenge(authz)
authz.call_validate(
client=self.client, challenge_type=challenge_type, wait=wait
)
authzs_with_challenges_to_wait_for.append(
(
authz,
challenge_type,
authz.find_challenge(challenge_type=challenge_type),
)
)
return authzs_with_challenges_to_wait_for
def wait_for_validation(self, authzs_to_wait_for: list[Authorization]) -> None:
wait_for_validation(authzs=authzs_to_wait_for, client=self.client)
def _download_alternate_chains(
self, cert: CertificateChain
) -> list[CertificateChain]:
alternate_chains = []
for alternate in cert.alternates:
try:
alt_cert = CertificateChain.download(client=self.client, url=alternate)
except ModuleFailException as e:
self.module.warn(
f"Error while downloading alternative certificate {alternate}: {e}"
)
continue
if alt_cert.cert is not None:
alternate_chains.append(alt_cert)
else:
self.module.warn(
f"Error while downloading alternative certificate {alternate}: no certificate found"
)
return alternate_chains
@t.overload
def download_certificate(
self, order: Order, *, download_all_chains: t.Literal[True] = True
) -> tuple[CertificateChain, list[CertificateChain]]: ...
@t.overload
def download_certificate(
self, order: Order, *, download_all_chains: t.Literal[False]
) -> tuple[CertificateChain, None]: ...
@t.overload
def download_certificate(
self, order: Order, *, download_all_chains: bool = True
) -> tuple[CertificateChain, list[CertificateChain] | None]: ...
def download_certificate(
self, order: Order, *, download_all_chains: bool = True
) -> tuple[CertificateChain, list[CertificateChain] | None]:
"""
Download certificate from a valid oder.
"""
if order.status != "valid":
raise ModuleFailException(
f"The order must be valid, but has state {order.status!r}!"
)
if not order.certificate_uri:
raise ModuleFailException(
f"Order's crtificate URL {order.certificate_uri!r} is empty!"
)
cert = CertificateChain.download(client=self.client, url=order.certificate_uri)
if cert.cert is None:
raise ModuleFailException(
f"Certificate at {order.certificate_uri} is empty!"
)
alternate_chains = None
if download_all_chains:
alternate_chains = self._download_alternate_chains(cert)
return cert, alternate_chains
@t.overload
def get_certificate(
self, order: Order, *, download_all_chains: t.Literal[True] = True
) -> tuple[CertificateChain, list[CertificateChain] | None]: ...
@t.overload
def get_certificate(
self, order: Order, *, download_all_chains: t.Literal[False]
) -> tuple[CertificateChain, list[CertificateChain] | None]: ...
@t.overload
def get_certificate(
self, order: Order, *, download_all_chains: bool = True
) -> tuple[CertificateChain, list[CertificateChain] | None]: ...
def get_certificate(
self, order: Order, *, download_all_chains: bool = True
) -> tuple[CertificateChain, list[CertificateChain] | None]:
"""
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.
"""
if self.csr is None and self.csr_content is None:
raise ModuleFailException("No CSR has been provided")
for authz in order.authorizations.values():
if authz.status != "valid":
authz.raise_error(
error_msg=f'Status is {authz.status!r} and not "valid"',
module=self.module,
)
order.finalize(
client=self.client,
csr_der=pem_to_der(pem_filename=self.csr, pem_content=self.csr_content),
)
return self.download_certificate(order, download_all_chains=download_all_chains)
def find_matching_chain(
self,
*,
chains: list[CertificateChain],
select_chain_matcher: t.Iterable[ChainMatcher],
) -> CertificateChain | None:
for criterium_idx, matcher in enumerate(select_chain_matcher):
for chain in chains:
if matcher.match(certificate=chain):
self.module.debug(
f"Found matching chain for criterium {criterium_idx}"
)
return chain
return None
def write_cert_chain(
self,
*,
cert: CertificateChain,
cert_dest: str | os.PathLike | None = None,
fullchain_dest: str | os.PathLike | None = None,
chain_dest: str | os.PathLike | None = None,
) -> bool:
changed = False
if cert.cert is None:
raise ValueError("Certificate is not present")
if cert_dest and write_file(
module=self.module, dest=cert_dest, content=cert.cert.encode("utf8")
):
changed = True
if fullchain_dest and write_file(
module=self.module,
dest=fullchain_dest,
content=(cert.cert + "\n".join(cert.chain)).encode("utf8"),
):
changed = True
if chain_dest and write_file(
module=self.module,
dest=chain_dest,
content=("\n".join(cert.chain)).encode("utf8"),
):
changed = True
return changed
def deactivate_authzs(self, order: Order) -> None:
"""
Deactivates all valid authz's. Does not raise exceptions.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2
"""
if len(order.authorization_uris) > len(order.authorizations):
for authz_uri in order.authorization_uris:
authz = None
try:
authz = Authorization.deactivate_url(
client=self.client, url=authz_uri
)
except Exception:
# ignore errors
pass
if authz is None or authz.status != "deactivated":
self.module.warn(
warning=f"Could not deactivate authz object {authz_uri}."
)
else:
for authz in order.authorizations.values():
try:
authz.deactivate(client=self.client)
except Exception:
# ignore errors
pass
if authz.status != "deactivated":
self.module.warn(
warning=f"Could not deactivate authz object {authz.url}."
)
__all__ = ("ACMECertificateClient",)

View File

@@ -0,0 +1,132 @@
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import abc
import typing as t
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
ModuleFailException,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.utils import (
der_to_pem,
process_links,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.pem import (
split_pem_list,
)
if t.TYPE_CHECKING:
from ansible_collections.community.crypto.plugins.module_utils._acme.acme import (
ACMEClient,
)
_CertificateChain = t.TypeVar("_CertificateChain", bound="CertificateChain")
class CertificateChain:
"""
Download and parse the certificate chain.
https://tools.ietf.org/html/rfc8555#section-7.4.2
"""
def __init__(self, url: str):
self.url = url
self.cert: str | None = None
self.chain: list[str] = []
self.alternates: list[str] = []
@classmethod
def download(
cls: t.Type[_CertificateChain], *, client: ACMEClient, url: str
) -> _CertificateChain:
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"
):
raise ModuleFailException(
f"Cannot download certificate chain from {url}, as content type is not application/pem-certificate-chain: {content!r} (headers: {info})"
)
result = cls(url)
# Parse data
certs = split_pem_list(content.decode("utf-8"), keep_inbetween=True)
if certs:
result.cert = certs[0]
result.chain = certs[1:]
process_links(
info=info,
callback=lambda link, relation: result._process_links( # pylint: disable=protected-access
client=client, link=link, relation=relation
),
)
if result.cert is None:
raise ModuleFailException(
f"Failed to parse certificate chain download from {url}: {content!r} (headers: {info})"
)
return result
def _process_links(self, *, client: ACMEClient, link: str, relation: str) -> None:
if relation == "up":
# Process link-up headers if there was no chain in reply
if not self.chain:
chain_result, chain_info = client.get_request(
link, parse_json_result=False
)
if chain_info["status"] in [200, 201]:
self.chain.append(der_to_pem(chain_result))
elif relation == "alternate":
self.alternates.append(link)
def to_json(self) -> dict[str, bytes]:
if self.cert is None:
raise ValueError("Has no certificate")
cert = self.cert.encode("utf8")
chain = ("\n".join(self.chain)).encode("utf8")
return {
"cert": cert,
"chain": chain,
"full_chain": cert + chain,
}
class Criterium:
def __init__(self, *, criterium: dict[str, t.Any], index: int):
self.index = index
self.test_certificates: t.Literal["first", "last", "all"] = criterium[
"test_certificates"
]
self.subject: dict[str, t.Any] | None = criterium["subject"]
self.issuer: dict[str, t.Any] | None = criterium["issuer"]
self.subject_key_identifier: str | None = criterium["subject_key_identifier"]
self.authority_key_identifier: str | None = criterium[
"authority_key_identifier"
]
class ChainMatcher(metaclass=abc.ABCMeta):
@abc.abstractmethod
def match(self, *, certificate: CertificateChain) -> bool:
"""
Check whether a certificate chain (CertificateChain instance) matches.
"""
__all__ = ("CertificateChain", "Criterium", "ChainMatcher")

View File

@@ -0,0 +1,414 @@
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import base64
import hashlib
import ipaddress
import json
import re
import time
import typing as t
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 (
nopad_b64,
)
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._acme.acme import (
ACMEClient,
)
def create_key_authorization(*, client: ACMEClient, token: str) -> str:
"""
Returns the key authorization for the given token
https://tools.ietf.org/html/rfc8555#section-8.1
"""
accountkey_json = json.dumps(
client.account_jwk, sort_keys=True, separators=(",", ":")
)
thumbprint = nopad_b64(hashlib.sha256(accountkey_json.encode("utf8")).digest())
return f"{token}.{thumbprint}"
def combine_identifier(*, identifier_type: str, identifier: str) -> str:
return f"{identifier_type}:{identifier}"
def normalize_combined_identifier(identifier: str) -> str:
identifier_type, identifier = split_identifier(identifier)
# Normalize DNS names and IPs
identifier = identifier.lower()
return combine_identifier(identifier_type=identifier_type, identifier=identifier)
def split_identifier(identifier: str) -> tuple[str, str]:
parts = identifier.split(":", 1)
if len(parts) != 2:
raise ModuleFailException(
f'Identifier "{identifier}" is not of the form <type>:<identifier>'
)
return parts[0], parts[1]
_Challenge = t.TypeVar("_Challenge", bound="Challenge")
class Challenge:
def __init__(self, *, data: dict[str, t.Any], url: str) -> None:
self.data = data
self.type: str = data["type"]
self.url = url
self.status: str = data["status"]
self.token: str | None = data.get("token")
@classmethod
def from_json(
cls: t.Type[_Challenge],
*,
client: ACMEClient,
data: dict[str, t.Any],
url: str | None = None,
) -> _Challenge:
return cls(data=data, url=url or data["url"])
def call_validate(self, client: ACMEClient) -> None:
challenge_response: dict[str, t.Any] = {}
client.send_signed_request(
self.url,
challenge_response,
error_msg="Failed to validate challenge",
expected_status_codes=[200, 202],
)
def to_json(self) -> dict[str, t.Any]:
return self.data.copy()
def get_validation_data(
self, *, client: ACMEClient, identifier_type: str, identifier: str
) -> dict[str, t.Any] | None:
if self.token is None:
return None
token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token)
key_authorization = create_key_authorization(client=client, token=token)
if self.type == "http-01":
# https://tools.ietf.org/html/rfc8555#section-8.3
return {
"resource": f".well-known/acme-challenge/{token}",
"resource_value": key_authorization,
}
if self.type == "dns-01":
if identifier_type != "dns":
return None
# https://tools.ietf.org/html/rfc8555#section-8.4
resource = "_acme-challenge"
value = nopad_b64(hashlib.sha256(to_bytes(key_authorization)).digest())
record = f"{resource}.{identifier[2:] if identifier.startswith('*.') else identifier}"
return {
"resource": resource,
"resource_value": value,
"record": record,
}
if self.type == "tls-alpn-01":
# https://www.rfc-editor.org/rfc/rfc8737.html#section-3
if identifier_type == "ip":
# IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596)
resource = ipaddress.ip_address(identifier).reverse_pointer
if not resource.endswith("."):
resource += "."
else:
resource = identifier
b_value = base64.b64encode(
hashlib.sha256(to_bytes(key_authorization)).digest()
)
return {
"resource": resource,
"resource_original": combine_identifier(
identifier_type=identifier_type, identifier=identifier
),
"resource_value": b_value,
}
# Unknown challenge type: ignore
return None
_Authorization = t.TypeVar("_Authorization", bound="Authorization")
class Authorization:
def __init__(self, *, url: str) -> None:
self.url = url
self.data: dict[str, t.Any] | None = None
self.challenges: list[Challenge] = []
self.status: str | None = None
self.identifier_type: str | None = None
self.identifier: str | None = None
def _setup(self, *, client: ACMEClient, data: dict[str, t.Any]) -> None:
data["uri"] = self.url
self.data = data
# While 'challenges' is a required field, apparently not every CA cares
# (https://github.com/ansible-collections/community.crypto/issues/824)
if data.get("challenges"):
self.challenges = [
Challenge.from_json(client=client, data=challenge)
for challenge in data["challenges"]
]
else:
self.challenges = []
self.status = data["status"]
self.identifier = data["identifier"]["value"]
self.identifier_type = data["identifier"]["type"]
if data.get("wildcard", False):
self.identifier = f"*.{self.identifier}"
@classmethod
def from_json(
cls: t.Type[_Authorization],
*,
client: ACMEClient,
data: dict[str, t.Any],
url: str,
) -> _Authorization:
result = cls(url=url)
result._setup(client=client, data=data)
return result
@classmethod
def from_url(
cls: t.Type[_Authorization], *, client: ACMEClient, url: str
) -> _Authorization:
result = cls(url=url)
result.refresh(client=client)
return result
@classmethod
def create(
cls: t.Type[_Authorization],
*,
client: ACMEClient,
identifier_type: str,
identifier: str,
) -> _Authorization:
"""
Create a new authorization for the given identifier.
Return the authorization object of the new authorization
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4
"""
new_authz = {
"identifier": {
"type": identifier_type,
"value": identifier,
},
}
if "newAuthz" not in client.directory.directory:
raise ACMEProtocolException(
module=client.module,
msg="ACME endpoint does not support pre-authorization",
)
url = client.directory["newAuthz"]
result, info = client.send_signed_request(
url,
new_authz,
error_msg="Failed to request challenges",
expected_status_codes=[200, 201],
)
return cls.from_json(client=client, data=result, url=info["location"])
@property
def combined_identifier(self) -> str:
if self.identifier_type is None or self.identifier is None:
raise ValueError("Data not present")
return combine_identifier(
identifier_type=self.identifier_type, identifier=self.identifier
)
def to_json(self) -> dict[str, t.Any]:
if self.data is None:
raise ValueError("Data not present")
return self.data.copy()
def refresh(self, *, client: ACMEClient) -> bool:
result, dummy = client.get_request(self.url)
changed = self.data != result
self._setup(client=client, data=result)
return changed
def get_challenge_data(self, *, client: ACMEClient) -> dict[str, t.Any]:
"""
Returns a dict with the data for all proposed (and supported) challenges
of the given authorization.
"""
if self.identifier_type is None or self.identifier is None:
raise ValueError("Data not present")
data = {}
for challenge in self.challenges:
validation_data = challenge.get_validation_data(
client=client,
identifier_type=self.identifier_type,
identifier=self.identifier,
)
if validation_data is not None:
data[challenge.type] = validation_data
return data
def raise_error(self, *, error_msg: str, module: AnsibleModule) -> t.NoReturn:
"""
Aborts with a specific error for a challenge.
"""
error_details = []
# multiple challenges could have failed at this point, gather error
# details for all of them before failing
for challenge in self.challenges:
if challenge.status == "invalid":
msg = f"Challenge {challenge.type}"
if "error" in challenge.data:
problem = format_error_problem(
challenge.data["error"],
subproblem_prefix=f"{challenge.type}.",
)
msg = f"{msg}: {problem}"
error_details.append(msg)
raise ACMEProtocolException(
module=module,
msg=f"Failed to validate challenge for {self.combined_identifier}: {error_msg}. {'; '.join(error_details)}",
extras={
"identifier": self.combined_identifier,
"authorization": self.data,
},
)
def find_challenge(self, *, challenge_type: str) -> Challenge | None:
for challenge in self.challenges:
if challenge_type == challenge.type:
return challenge
return None
def wait_for_validation(self, *, client: ACMEClient) -> bool:
while True:
self.refresh(client=client)
if self.status in ["valid", "invalid", "revoked"]:
break
time.sleep(2)
if self.status == "invalid":
self.raise_error(error_msg='Status is "invalid"', module=client.module)
return self.status == "valid"
def call_validate(
self, *, client: ACMEClient, challenge_type: str, wait: bool = True
) -> bool:
"""
Validate the authorization provided in the auth dict. Returns True
when the validation was successful and False when it was not.
"""
challenge = self.find_challenge(challenge_type=challenge_type)
if challenge is None:
raise ModuleFailException(
f'Found no challenge of type "{challenge_type}" for identifier {self.combined_identifier}!'
)
challenge.call_validate(client)
if not wait:
return self.status == "valid"
return self.wait_for_validation(client=client)
def can_deactivate(self) -> bool:
"""
Deactivates this authorization.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2
"""
return self.status in ("valid", "pending")
def deactivate(self, *, client: ACMEClient) -> bool | None:
"""
Deactivates this authorization.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2
"""
if not self.can_deactivate():
return None
authz_deactivate = {"status": "deactivated"}
result, info = client.send_signed_request(
self.url, authz_deactivate, fail_on_error=False
)
if 200 <= info["status"] < 300 and result.get("status") == "deactivated":
self.status = "deactivated"
return True
return False
@classmethod
def deactivate_url(
cls: t.Type[_Authorization], *, client: ACMEClient, url: str
) -> _Authorization:
"""
Deactivates this authorization.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2
"""
authz = cls(url=url)
authz_deactivate = {"status": "deactivated"}
result, _info = client.send_signed_request(
url, authz_deactivate, fail_on_error=True
)
authz._setup(client=client, data=result)
return authz
def wait_for_validation(
*, authzs: t.Iterable[Authorization], client: ACMEClient
) -> None:
"""
Wait until a list of authz is valid. Fail if at least one of them is invalid or revoked.
"""
while authzs:
authzs_next = []
for authz in authzs:
authz.refresh(client=client)
if authz.status in ["valid", "invalid", "revoked"]:
if authz.status != "valid":
authz.raise_error(
error_msg='Status is not "valid"', module=client.module
)
else:
authzs_next.append(authz)
if authzs_next:
time.sleep(2)
authzs = authzs_next
__all__ = (
"create_key_authorization",
"combine_identifier",
"normalize_combined_identifier",
"split_identifier",
"Challenge",
"Authorization",
"wait_for_validation",
)

View File

@@ -0,0 +1,184 @@
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import typing as t
from http.client import responses as http_responses
from ansible.module_utils.common.text.converters import to_text
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
def format_http_status(status_code: int) -> str:
expl = http_responses.get(status_code)
if not expl:
return str(status_code)
return f"{status_code} {expl}"
def format_error_problem(
problem: dict[str, t.Any], *, subproblem_prefix: str = ""
) -> str:
error_type = problem.get(
"type", "about:blank"
) # https://www.rfc-editor.org/rfc/rfc7807#section-3.1
if "title" in problem:
msg = f'Error "{problem["title"]}" ({error_type})'
else:
msg = f"Error {error_type}"
if "detail" in problem:
msg += f': "{problem["detail"]}"'
subproblems = problem.get("subproblems")
if subproblems is not None:
msg = f"{msg} Subproblems:"
for index, subproblem in enumerate(subproblems):
index_str = f"{subproblem_prefix}{index}"
subproblem_str = format_error_problem(
subproblem, subproblem_prefix=f"{index_str}."
)
msg = f"{msg}\n({index_str}) {subproblem_str}"
return msg
class ModuleFailException(Exception):
"""
If raised, module.fail_json() will be called with the given parameters after cleanup.
"""
def __init__(self, msg: str, **args: t.Any) -> None:
super().__init__(self, msg)
self.msg = msg
self.module_fail_args = args
def do_fail(self, *, module: AnsibleModule, **arguments) -> t.NoReturn:
module.fail_json(msg=self.msg, other=self.module_fail_args, **arguments)
class ACMEProtocolException(ModuleFailException):
def __init__(
self,
*,
module: AnsibleModule,
msg: str | None = None,
info: dict[str, t.Any] | None = None,
response=None,
content: bytes | None = None,
content_json: dict[str, t.Any] | None = None,
extras: dict[str, t.Any] | None = None,
):
# Try to get hold of content, if response is given and content is not provided
if content is None and content_json is None and response is not None:
try:
# In Python 2, reading from a closed response yields a TypeError.
# In Python 3, read() simply returns ''
if response.closed:
raise TypeError
content = response.read()
except (AttributeError, TypeError):
if info is not None:
content = info.pop("body", None)
# Make sure that content_json is None or a dictionary
if content_json is not None and not isinstance(content_json, dict):
if content is None and isinstance(content_json, bytes):
content = content_json
content_json = None
# Try to get hold of JSON decoded content, when content is given and JSON not provided
if content_json is None and content is not None and module is not None:
try:
content_json = module.from_json(to_text(content))
except Exception:
pass
extras = extras or {}
error_code = None
error_type = None
if msg is None:
msg = "ACME request failed"
add_msg = ""
if info is not None:
url = info["url"]
code = info["status"]
extras["http_url"] = url
extras["http_status"] = code
error_code = code
if (
code is not None
and code >= 400
and content_json is not None
and "type" in content_json
):
error_type = content_json["type"]
if "status" in content_json and content_json["status"] != code:
code_msg = f"status {content_json['status']} (HTTP status: {format_http_status(code)})"
else:
code_msg = f"status {format_http_status(code)}"
if code == -1 and info.get("msg"):
code_msg = f"error: {info['msg']}"
subproblems = content_json.pop("subproblems", None)
add_msg = f" {format_error_problem(content_json)}."
extras["problem"] = content_json
extras["subproblems"] = subproblems or []
if subproblems is not None:
add_msg = f"{add_msg} Subproblems:"
for index, problem in enumerate(subproblems):
problem = format_error_problem(
problem, subproblem_prefix=f"{index}."
)
add_msg = f"{add_msg}\n({index}) {problem}."
else:
code_msg = f"HTTP status {format_http_status(code)}"
if code == -1 and info.get("msg"):
code_msg = f"error: {info['msg']}"
if content_json is not None:
add_msg = f" The JSON error result: {content_json}"
elif content is not None:
add_msg = f" The raw error result: {to_text(content)}"
msg = f"{msg} for {url} with {code_msg}"
elif content_json is not None:
add_msg = f" The JSON result: {content_json}"
elif content is not None:
add_msg = f" The raw result: {to_text(content)}"
super().__init__(f"{msg}.{add_msg}", **extras)
self.problem: dict[str, t.Any] = {}
self.subproblems: list[dict[str, t.Any]] = []
self.error_code = error_code
self.error_type = error_type
for k, v in extras.items():
setattr(self, k, v)
class BackendException(ModuleFailException):
pass
class NetworkException(ModuleFailException):
pass
class KeyParsingError(ModuleFailException):
pass
__all__ = (
"format_http_status",
"format_error_problem",
"ModuleFailException",
"ACMEProtocolException",
"BackendException",
"NetworkException",
"KeyParsingError",
)

View File

@@ -1,52 +1,61 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2013, Romeo Theriault <romeot () hawaii.edu> # Copyright (c) 2013, Romeo Theriault <romeot () hawaii.edu>
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> # Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright (c) 2021 Felix Fontein <felix@fontein.de> # Copyright (c) 2021 Felix Fontein <felix@fontein.de>
# 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 # Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
__metaclass__ = type # Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import os import os
import shutil import shutil
import tempfile import tempfile
import traceback import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_native from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
ModuleFailException,
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException )
def read_file(fn, mode='b'): if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
def read_file(fn: str | os.PathLike) -> bytes:
try: try:
with open(fn, 'r' + mode) as f: with open(fn, "rb") 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(f'Error while reading file "{fn}": {e}') from e
# 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: AnsibleModule, dest: str | os.PathLike, content: bytes
) -> bool:
"""
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(
f"failed to create temporary content file: {err}",
exception=traceback.format_exc(),
) from err
f.close() f.close()
checksum_src = None checksum_src = None
checksum_dest = None checksum_dest = None
@@ -54,34 +63,40 @@ 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(f"Source {tmpsrc} does not exist")
if not os.access(tmpsrc, os.R_OK): if not os.access(tmpsrc, os.R_OK):
os.remove(tmpsrc) os.remove(tmpsrc)
raise ModuleFailException("Source %s not readable" % (tmpsrc)) raise ModuleFailException(f"Source {tmpsrc} not readable")
checksum_src = module.sha1(tmpsrc) checksum_src = module.sha1(tmpsrc)
# check if there is no dest file # check if there is no dest file
if os.path.exists(dest): if os.path.exists(dest):
# raise an error if copy has no permission on dest # raise an error if copy has no permission on dest
if not os.access(dest, os.W_OK): if not os.access(dest, os.W_OK):
os.remove(tmpsrc) os.remove(tmpsrc)
raise ModuleFailException("Destination %s not writable" % (dest)) raise ModuleFailException(f"Destination {dest} not writable")
if not os.access(dest, os.R_OK): if not os.access(dest, os.R_OK):
os.remove(tmpsrc) os.remove(tmpsrc)
raise ModuleFailException("Destination %s not readable" % (dest)) raise ModuleFailException(f"Destination {dest} not readable")
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(f"Destination dir {dirname} not writable")
if checksum_src != checksum_dest: if checksum_src != checksum_dest:
try: try:
shutil.copyfile(tmpsrc, dest) shutil.copyfile(tmpsrc, dest)
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(
f"failed to copy {tmpsrc} to {dest}: {err}",
exception=traceback.format_exc(),
) from err
os.remove(tmpsrc) os.remove(tmpsrc)
return changed return changed
__all__ = ("read_file", "write_file")

View File

@@ -0,0 +1,231 @@
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import time
import typing as t
from ansible_collections.community.crypto.plugins.module_utils._acme.challenges import (
Authorization,
normalize_combined_identifier,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
ACMEProtocolException,
ModuleFailException,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.utils import (
nopad_b64,
)
if t.TYPE_CHECKING:
from ansible_collections.community.crypto.plugins.module_utils._acme.acme import (
ACMEClient,
)
_Order = t.TypeVar("_Order", bound="Order")
class Order:
def __init__(self, *, url: str) -> None:
self.url = url
self.data: dict[str, t.Any] | None = None
self.status = None
self.identifiers: list[tuple[str, str]] = []
self.replaces_cert_id = None
self.finalize_uri = None
self.certificate_uri = None
self.authorization_uris: list[str] = []
self.authorizations: dict[str, Authorization] = {}
def _setup(self, *, client: ACMEClient, data: dict[str, t.Any]) -> None:
self.data = data
self.status = data["status"]
self.identifiers = []
for identifier in data["identifiers"]:
self.identifiers.append((identifier["type"], identifier["value"]))
self.replaces_cert_id = data.get("replaces")
self.finalize_uri = data.get("finalize")
self.certificate_uri = data.get("certificate")
self.authorization_uris = data["authorizations"]
self.authorizations = {}
@classmethod
def from_json(
cls: t.Type[_Order], *, client: ACMEClient, data: dict[str, t.Any], url: str
) -> _Order:
result = cls(url=url)
result._setup(client=client, data=data)
return result
@classmethod
def from_url(cls: t.Type[_Order], *, client: ACMEClient, url: str) -> _Order:
result = cls(url=url)
result.refresh(client=client)
return result
@classmethod
def create(
cls: t.Type[_Order],
*,
client: ACMEClient,
identifiers: list[tuple[str, str]],
replaces_cert_id: str | None = None,
profile: str | None = None,
) -> _Order:
"""
Start a new certificate order (ACME v2 protocol).
https://tools.ietf.org/html/rfc8555#section-7.4
"""
acme_identifiers = []
for identifier_type, identifier in identifiers:
acme_identifiers.append(
{
"type": identifier_type,
"value": identifier,
}
)
new_order: dict[str, t.Any] = {"identifiers": acme_identifiers}
if replaces_cert_id is not None:
new_order["replaces"] = replaces_cert_id
if profile is not None:
new_order["profile"] = profile
result, info = client.send_signed_request(
client.directory["newOrder"],
new_order,
error_msg="Failed to start new order",
expected_status_codes=[201],
)
return cls.from_json(client=client, data=result, url=info["location"])
@classmethod
def create_with_error_handling(
cls: t.Type[_Order],
*,
client: ACMEClient,
identifiers: list[tuple[str, str]],
error_strategy: t.Literal[
"auto", "fail", "always", "retry_without_replaces_cert_id"
] = "auto",
error_max_retries: int = 3,
replaces_cert_id: str | None = None,
profile: str | None = None,
message_callback: t.Callable[[str], None] | None = None,
) -> _Order:
"""
error_strategy can be one of the following strings:
* ``fail``: simply fail. (Same behavior as ``Order.create()``.)
* ``retry_without_replaces_cert_id``: if ``replaces_cert_id`` is not ``None``, set it to ``None`` and retry.
The only exception is an error of type ``urn:ietf:params:acme:error:alreadyReplaced``, that indicates that
the certificate was already replaced.
* ``auto``: try to be clever. Right now this is identical to ``retry_without_replaces_cert_id``, but that can
change at any time in the future.
* ``always``: always retry until ``error_max_retries`` has been reached.
"""
tries = 0
while True:
tries += 1
try:
return cls.create(
client=client,
identifiers=identifiers,
replaces_cert_id=replaces_cert_id,
profile=profile,
)
except ACMEProtocolException as exc:
if tries <= error_max_retries + 1 and error_strategy != "fail":
if error_strategy == "always":
continue
if (
error_strategy in ("auto", "retry_without_replaces_cert_id")
and replaces_cert_id is not None
and not (
exc.error_code == 409
and exc.error_type
== "urn:ietf:params:acme:error:alreadyReplaced"
)
):
if message_callback:
message_callback(
f"Stop passing `replaces={replaces_cert_id}` due to error {exc.error_code} {exc.error_type} when creating ACME order"
)
replaces_cert_id = None
continue
raise
def refresh(self, *, client: ACMEClient) -> bool:
result, dummy = client.get_request(self.url)
changed = self.data != result
self._setup(client=client, data=result)
return changed
def load_authorizations(self, *, client: ACMEClient) -> None:
for auth_uri in self.authorization_uris:
authz = Authorization.from_url(client=client, url=auth_uri)
self.authorizations[
normalize_combined_identifier(authz.combined_identifier)
] = authz
def wait_for_finalization(self, *, client: ACMEClient) -> None:
while True:
self.refresh(client=client)
if self.status in ["valid", "invalid", "pending", "ready"]:
break
time.sleep(2)
if self.status != "valid":
raise ACMEProtocolException(
module=client.module,
msg=f'Failed to wait for order to complete; got status "{self.status}"',
content_json=self.data,
)
def finalize(
self, *, client: ACMEClient, csr_der: bytes, wait: bool = True
) -> None:
"""
Create a new certificate based on the csr.
Return the certificate object as dict
https://tools.ietf.org/html/rfc8555#section-7.4
"""
if self.finalize_uri is None:
raise ModuleFailException("finalize_uri must be set")
new_cert = {
"csr": nopad_b64(csr_der),
}
result, info = client.send_signed_request(
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.
# Instead of using the result, we call self.refresh(client) below.
if wait:
self.wait_for_finalization(client=client)
else:
self.refresh(client=client)
if self.status not in ["procesing", "valid", "invalid"]:
raise ACMEProtocolException(
module=client.module,
msg=f'Failed to finalize order; got status "{self.status}"',
info=info,
content_json=result,
)
__all__ = ("Order",)

View File

@@ -0,0 +1,173 @@
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import base64
import datetime
import os
import re
import textwrap
import traceback
import typing as t
from urllib.parse import unquote
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
ModuleFailException,
)
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,
)
if t.TYPE_CHECKING:
from ansible_collections.community.crypto.plugins.module_utils._acme.backends import (
CertificateInformation,
CryptoBackend,
)
def nopad_b64(data: bytes) -> str:
return base64.urlsafe_b64encode(data).decode("utf8").replace("=", "")
def der_to_pem(der_cert: bytes) -> str:
"""
Convert the DER format certificate in der_cert to a PEM format certificate and return it.
"""
content = "\n".join(textwrap.wrap(base64.b64encode(der_cert).decode("utf8"), 64))
return f"-----BEGIN CERTIFICATE-----\n{content}\n-----END CERTIFICATE-----\n"
def pem_to_der(
*, pem_filename: str | os.PathLike | None = None, pem_content: str | None = None
) -> bytes:
"""
Load PEM file, or use PEM file's content, and convert to DER.
If PEM contains multiple entities, the first entity will be used.
"""
certificate_lines = []
if pem_content is not None:
lines = pem_content.splitlines()
elif pem_filename is not None:
try:
with open(pem_filename, "r", encoding="utf-8") as f:
lines = list(f)
except Exception as err:
raise ModuleFailException(
f"cannot load PEM file {pem_filename}: {err}",
exception=traceback.format_exc(),
) from err
else:
raise ModuleFailException(
"One of pem_filename and pem_content must be provided"
)
header_line_count = 0
for line in lines:
if line.startswith("-----"):
header_line_count += 1
if header_line_count == 2:
# If certificate file contains other certs appended
# (like intermediate certificates), ignore these.
break
continue
certificate_lines.append(line.strip())
return base64.b64decode("".join(certificate_lines))
def process_links(
*, info: dict[str, t.Any], callback: t.Callable[[str, str], None]
) -> None:
"""
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
"""
if "link" in info:
link = info["link"]
for url, relation in re.findall(r'<([^>]+)>;\s*rel="(\w+)"', link):
callback(unquote(url), relation)
def parse_retry_after(
value: str,
*,
relative_with_timezone: bool = True,
now: datetime.datetime | None = None,
) -> datetime.datetime:
"""
Parse the value of a Retry-After header and return a timestamp.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
"""
# First try a number of seconds
try:
delta = datetime.timedelta(seconds=int(value))
if now is None:
now = get_now_datetime(with_timezone=relative_with_timezone)
return now + delta
except ValueError:
pass
try:
return datetime.datetime.strptime(value, "%a, %d %b %Y %H:%M:%S GMT")
except ValueError:
pass
raise ValueError(f"Cannot parse Retry-After header value {repr(value)}")
def compute_cert_id(
*,
backend: CryptoBackend,
cert_info: CertificateInformation | None = None,
cert_filename: str | os.PathLike | None = None,
cert_content: str | bytes | None = None,
none_if_required_information_is_missing: bool = False,
) -> str | None:
# Obtain certificate info if not provided
if cert_info is None:
cert_info = backend.get_cert_information(
cert_filename=cert_filename, cert_content=cert_content
)
# Convert Authority Key Identifier to string
if cert_info.authority_key_identifier is None:
if none_if_required_information_is_missing:
return None
raise ModuleFailException(
"Certificate has no Authority Key Identifier extension"
)
aki = (
(base64.urlsafe_b64encode(cert_info.authority_key_identifier))
.decode("ascii")
.replace("=", "")
)
# Convert serial number to string
serial_bytes = convert_int_to_bytes(cert_info.serial_number)
if ord(serial_bytes[:1]) >= 128:
serial_bytes = b"\x00" + serial_bytes
serial = (base64.urlsafe_b64encode(serial_bytes)).decode("ascii").replace("=", "")
# Compose cert ID
return f"{aki}.{serial}"
__all__ = (
"nopad_b64",
"der_to_pem",
"pem_to_der",
"process_links",
"parse_retry_after",
"compute_cert_id",
)

View File

@@ -0,0 +1,124 @@
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import typing as t
from ansible.module_utils.basic import AnsibleModule
_T = t.TypeVar("_T")
def _ensure_list(value: list[_T] | tuple[_T] | None) -> list[_T]:
if value is None:
return []
return list(value)
class ArgumentSpec:
def __init__(
self,
argument_spec: dict[str, t.Any] | None = None,
*,
mutually_exclusive: list[list[str] | tuple[str, ...]] | None = None,
required_together: list[list[str] | tuple[str, ...]] | None = None,
required_one_of: list[list[str] | tuple[str, ...]] | None = None,
required_if: (
list[
tuple[str, t.Any, list[str] | tuple[str, ...]]
| tuple[str, t.Any, list[str] | tuple[str, ...], bool]
]
| None
) = None,
required_by: dict[str, tuple[str, ...] | list[str]] | None = None,
) -> None:
self.argument_spec = argument_spec or {}
self.mutually_exclusive = _ensure_list(mutually_exclusive)
self.required_together = _ensure_list(required_together)
self.required_one_of = _ensure_list(required_one_of)
self.required_if = _ensure_list(required_if)
self.required_by = required_by or {}
def update_argspec(self, **kwargs) -> t.Self:
self.argument_spec.update(kwargs)
return self
def update(
self,
*,
mutually_exclusive: list[list[str] | tuple[str, ...]] | None = None,
required_together: list[list[str] | tuple[str, ...]] | None = None,
required_one_of: list[list[str] | tuple[str, ...]] | None = None,
required_if: (
list[
tuple[str, t.Any, list[str] | tuple[str, ...]]
| tuple[str, t.Any, list[str] | tuple[str, ...], bool]
]
| None
) = None,
required_by: dict[str, tuple[str, ...] | list[str]] | None = None,
):
if mutually_exclusive:
self.mutually_exclusive.extend(mutually_exclusive)
if required_together:
self.required_together.extend(required_together)
if required_one_of:
self.required_one_of.extend(required_one_of)
if required_if:
self.required_if.extend(required_if)
if required_by:
for k, v in required_by.items():
if k in self.required_by:
v = list(self.required_by[k]) + list(v)
self.required_by[k] = v
return self
def merge(self, other: t.Self) -> t.Self:
self.update_argspec(**other.argument_spec)
self.update(
mutually_exclusive=other.mutually_exclusive,
required_together=other.required_together,
required_one_of=other.required_one_of,
required_if=other.required_if,
required_by=other.required_by,
)
return self
def create_ansible_module_helper(
self, clazz: type[_T], args: tuple, **kwargs: t.Any
) -> _T:
for forbidden_name in (
"argument_spec",
"mutually_exclusive",
"required_together",
"required_one_of",
"required_if",
"required_by",
):
if forbidden_name in kwargs:
raise ValueError(
f"You must not provide a {forbidden_name} keyword parameter to create_ansible_module_helper()"
)
instance = clazz( # type: ignore
*args,
argument_spec=self.argument_spec,
mutually_exclusive=self.mutually_exclusive,
required_together=self.required_together,
required_one_of=self.required_one_of,
required_if=self.required_if,
required_by=self.required_by,
**kwargs,
)
return instance
def create_ansible_module(self, **kwargs: t.Any) -> AnsibleModule:
return self.create_ansible_module_helper(AnsibleModule, (), **kwargs)
__all__ = ("ArgumentSpec",)

View File

@@ -0,0 +1,176 @@
# Copyright (c) 2020, Jordan Borean <jborean93@gmail.com>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import enum
import re
from ansible.module_utils.common.text.converters import to_bytes
# An ASN.1 serialized as a string in the OpenSSL format:
# [modifier,]type[:value]
#
# 'modifier':
# The modifier can be 'IMPLICIT:<tag_number><tag_class>,' or 'EXPLICIT:<tag_number><tag_class>' where IMPLICIT
# changes the tag of the universal value to encode and EXPLICIT prefixes its tag to the existing universal value.
# The tag_number must be set while the tag_class can be 'U', 'A', 'P', or 'C" for 'Universal', 'Application',
# 'Private', or 'Context Specific' with C being the default.
#
# 'type':
# The underlying ASN.1 type of the value specified. Currently only the following have been implemented:
# UTF8: The value must be a UTF-8 encoded string.
#
# 'value':
# 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)?,)?"
r"(?P<value_type>[\w\d]+):(?P<value>.*)"
)
class TagClass(enum.Enum):
universal = 0
application = 1
context_specific = 2
private = 3
# Universal tag numbers that can be encoded.
class TagNumber(enum.Enum):
utf8_string = 12
def _pack_octet_integer(value: int) -> bytes:
"""Packs an integer value into 1 or multiple octets."""
# NOTE: This is *NOT* the same as packing an ASN.1 INTEGER like value.
octets = bytearray()
# Continue to shift the number by 7 bits and pack into an octet until the
# value is fully packed.
while value:
octet_value = value & 0b01111111
# First round (last octet) must have the MSB set.
if len(octets):
octet_value |= 0b10000000
octets.append(octet_value)
value >>= 7
# Reverse to ensure the higher order octets are first.
octets.reverse()
return bytes(octets)
def serialize_asn1_string_as_der(value: str) -> bytes:
"""Deserializes an ASN.1 string to a DER encoded byte string."""
asn1_match = ASN1_STRING_REGEX.match(value)
if not asn1_match:
raise ValueError(
"The ASN.1 serialized string must be in the format [modifier,]type[:value]"
)
tag_type = asn1_match.group("tag_type")
tag_number = asn1_match.group("tag_number")
tag_class = asn1_match.group("tag_class") or "C"
value_type = asn1_match.group("value_type")
asn1_value = asn1_match.group("value")
if value_type != "UTF8":
raise ValueError(
f'The ASN.1 serialized string is not a known type "{value_type}", only UTF8 types are supported'
)
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.
if not tag_type or (tag_type == "EXPLICIT" and tag_class != "U"):
b_value = pack_asn1(
tag_class=TagClass.universal,
constructed=False,
tag_number=TagNumber.utf8_string,
b_data=b_value,
)
if tag_type:
tag_class_enum = {
"U": TagClass.universal,
"A": TagClass.application,
"P": TagClass.private,
"C": TagClass.context_specific,
}[tag_class]
# 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_enum != TagClass.universal
b_value = pack_asn1(
tag_class=tag_class_enum,
constructed=constructed,
tag_number=int(tag_number),
b_data=b_value,
)
return b_value
def pack_asn1(
*,
tag_class: TagClass,
constructed: bool,
tag_number: TagNumber | int,
b_data: bytes,
) -> bytes:
"""Pack the value into an ASN.1 data structure.
The structure for an ASN.1 element is
| Identifier Octet(s) | Length Octet(s) | Data Octet(s) |
"""
b_asn1_data = bytearray()
# Bit 8 and 7 denotes the class.
identifier_octets = tag_class.value << 6
# Bit 6 denotes whether the value is primitive or constructed.
identifier_octets |= (1 if constructed else 0) << 5
# 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.
if isinstance(tag_number, TagNumber):
tag_number = tag_number.value
if tag_number < 31:
identifier_octets |= tag_number
b_asn1_data.append(identifier_octets)
else:
identifier_octets |= 31
b_asn1_data.append(identifier_octets)
b_asn1_data.extend(_pack_octet_integer(tag_number))
length = len(b_data)
# If the length can be encoded in 7 bits only 1 octet is required.
if length < 128:
b_asn1_data.append(length)
else:
# Otherwise the length must be encoded across multiple octets
length_octets = bytearray()
while length:
length_octets.append(length & 0b11111111)
length >>= 8
length_octets.reverse() # Reverse to make the higher octets first.
# The first length octet must have the MSB set alongside the number of
# octets the length was encoded in.
b_asn1_data.append(len(length_octets) | 0b10000000)
b_asn1_data.extend(length_octets)
return bytes(b_asn1_data) + b_data
__all__ = ("TagClass", "TagNumber", "serialize_asn1_string_as_der", "pack_asn1")

View File

@@ -4,6 +4,9 @@
# dynamically by Ansible, still belong to the author of the module, and may assign # dynamically by Ansible, still belong to the author of the module, and may assign
# their own license to the complete work. # their own license to the complete work.
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
# This excerpt is dual licensed under the terms of the Apache License, Version # This excerpt is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file at # 2.0, and the BSD License. See the LICENSE file at
# https://github.com/pyca/cryptography/blob/master/LICENSE for complete details. # https://github.com/pyca/cryptography/blob/master/LICENSE for complete details.
@@ -26,15 +29,15 @@
# pyca/cryptography@3057f91ea9a05fb593825006d87a391286a4d828 # pyca/cryptography@3057f91ea9a05fb593825006d87a391286a4d828
# pyca/cryptography@d607dd7e5bc5c08854ec0c9baff70ba4a35be36f # pyca/cryptography@d607dd7e5bc5c08854ec0c9baff70ba4a35be36f
from __future__ import absolute_import, division, print_function from __future__ import annotations
__metaclass__ = type
# WARNING: this function no longer works with cryptography 35.0.0 and newer! # WARNING: this function no longer works with cryptography 35.0.0 and newer!
# 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) -> str:
# 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
# #
@@ -55,3 +58,6 @@ def obj2txt(openssl_lib, openssl_ffi, obj):
buf = openssl_ffi.new("char[]", buf_len) buf = openssl_ffi.new("char[]", buf_len)
res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1) res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1)
return openssl_ffi.buffer(buf, res)[:].decode() return openssl_ffi.buffer(buf, res)[:].decode()
__all__ = ("obj2txt",)

View File

@@ -0,0 +1,38 @@
# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
from ansible_collections.community.crypto.plugins.module_utils._crypto._objects_data import (
OID_MAP,
)
OID_LOOKUP: dict[str, str] = {}
NORMALIZE_NAMES: dict[str, str] = {}
NORMALIZE_NAMES_SHORT: dict[str, str] = {}
for dotted, names in OID_MAP.items():
for name in names:
if name in NORMALIZE_NAMES and OID_LOOKUP[name] != dotted:
raise AssertionError(
f'Name collision during setup: "{name}" for OIDs {dotted} and {OID_LOOKUP[name]}'
)
NORMALIZE_NAMES[name] = names[0]
NORMALIZE_NAMES_SHORT[name] = names[-1]
OID_LOOKUP[name] = dotted
for alias, original in [("userID", "userId")]:
if alias in NORMALIZE_NAMES:
raise AssertionError(
f'Name collision during adding aliases: "{alias}" (alias for "{original}") is already mapped to OID {OID_LOOKUP[alias]}'
)
NORMALIZE_NAMES[alias] = original
NORMALIZE_NAMES_SHORT[alias] = NORMALIZE_NAMES_SHORT[original]
OID_LOOKUP[alias] = OID_LOOKUP[original]
__all__ = ("OID_LOOKUP", "NORMALIZE_NAMES", "NORMALIZE_NAMES_SHORT")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
try:
import cryptography # noqa: F401, pylint: disable=unused-import
HAS_CRYPTOGRAPHY = True
except ImportError:
# Error handled in the calling module.
HAS_CRYPTOGRAPHY = False
class OpenSSLObjectError(Exception):
pass
class OpenSSLBadPassphraseError(OpenSSLObjectError):
pass
__all__ = ("HAS_CRYPTOGRAPHY", "OpenSSLObjectError", "OpenSSLBadPassphraseError")

View File

@@ -0,0 +1,210 @@
# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import typing as t
from ansible_collections.community.crypto.plugins.module_utils._version import (
LooseVersion as _LooseVersion,
)
try:
import cryptography
from cryptography import x509
except ImportError:
# Error handled in the calling module.
pass
from ansible_collections.community.crypto.plugins.module_utils._crypto._obj2txt import (
obj2txt,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
HAS_CRYPTOGRAPHY,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_decode_name,
)
if t.TYPE_CHECKING:
import datetime
# TODO: once cryptography has a _utc variant of InvalidityDate.invalidity_date, set this
# to True and adjust get_invalidity_date() accordingly.
# (https://github.com/pyca/cryptography/issues/10818)
CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE = False
if HAS_CRYPTOGRAPHY:
CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE = _LooseVersion(
cryptography.__version__
) >= _LooseVersion("43.0.0")
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
if HAS_CRYPTOGRAPHY:
REVOCATION_REASON_MAP = {
"unspecified": x509.ReasonFlags.unspecified,
"key_compromise": x509.ReasonFlags.key_compromise,
"ca_compromise": x509.ReasonFlags.ca_compromise,
"affiliation_changed": x509.ReasonFlags.affiliation_changed,
"superseded": x509.ReasonFlags.superseded,
"cessation_of_operation": x509.ReasonFlags.cessation_of_operation,
"certificate_hold": x509.ReasonFlags.certificate_hold,
"privilege_withdrawn": x509.ReasonFlags.privilege_withdrawn,
"aa_compromise": x509.ReasonFlags.aa_compromise,
"remove_from_crl": x509.ReasonFlags.remove_from_crl,
}
REVOCATION_REASON_MAP_INVERSE = {}
for k, v in REVOCATION_REASON_MAP.items():
REVOCATION_REASON_MAP_INVERSE[v] = k
else:
REVOCATION_REASON_MAP = {}
REVOCATION_REASON_MAP_INVERSE = {}
def cryptography_decode_revoked_certificate(
cert: x509.RevokedCertificate,
) -> dict[str, t.Any]:
result = {
"serial_number": cert.serial_number,
"revocation_date": get_revocation_date(cert),
"issuer": None,
"issuer_critical": False,
"reason": None,
"reason_critical": False,
"invalidity_date": None,
"invalidity_date_critical": False,
}
try:
ext_ci = cert.extensions.get_extension_for_class(x509.CertificateIssuer)
result["issuer"] = list(ext_ci.value)
result["issuer_critical"] = ext_ci.critical
except x509.ExtensionNotFound:
pass
try:
ext_cr = cert.extensions.get_extension_for_class(x509.CRLReason)
result["reason"] = ext_cr.value.reason
result["reason_critical"] = ext_cr.critical
except x509.ExtensionNotFound:
pass
try:
ext_id = cert.extensions.get_extension_for_class(x509.InvalidityDate)
result["invalidity_date"] = get_invalidity_date(ext_id.value)
result["invalidity_date_critical"] = ext_id.critical
except x509.ExtensionNotFound:
pass
return result
def cryptography_dump_revoked(
entry: dict[str, t.Any],
*,
idn_rewrite: t.Literal["ignore", "idna", "unicode"] = "ignore",
) -> dict[str, t.Any]:
return {
"serial_number": entry["serial_number"],
"revocation_date": entry["revocation_date"].strftime(TIMESTAMP_FORMAT),
"issuer": (
[
cryptography_decode_name(issuer, idn_rewrite=idn_rewrite)
for issuer in entry["issuer"]
]
if entry["issuer"] is not None
else None
),
"issuer_critical": entry["issuer_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"],
}
def cryptography_get_signature_algorithm_oid_from_crl(
crl: x509.CertificateRevocationList,
) -> x509.oid.ObjectIdentifier:
try:
return crl.signature_algorithm_oid
except AttributeError:
# Older cryptography versions do not have signature_algorithm_oid yet
dotted = obj2txt(
crl._backend._lib, # type: ignore[attr-defined] # pylint: disable=protected-access
crl._backend._ffi, # type: ignore[attr-defined] # pylint: disable=protected-access
crl._x509_crl.sig_alg.algorithm, # type: ignore[attr-defined] # pylint: disable=protected-access
)
return x509.oid.ObjectIdentifier(dotted)
def get_next_update(obj: x509.CertificateRevocationList) -> datetime.datetime | None:
if CRYPTOGRAPHY_TIMEZONE:
return obj.next_update_utc
return obj.next_update
def get_last_update(obj: x509.CertificateRevocationList) -> datetime.datetime:
if CRYPTOGRAPHY_TIMEZONE:
return obj.last_update_utc
return obj.last_update
def get_revocation_date(obj: x509.RevokedCertificate) -> datetime.datetime:
if CRYPTOGRAPHY_TIMEZONE:
return obj.revocation_date_utc
return obj.revocation_date
def get_invalidity_date(obj: x509.InvalidityDate) -> datetime.datetime:
if CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE:
return obj.invalidity_date_utc
return obj.invalidity_date
def set_next_update(
builder: x509.CertificateRevocationListBuilder, *, value: datetime.datetime
) -> x509.CertificateRevocationListBuilder:
return builder.next_update(value)
def set_last_update(
builder: x509.CertificateRevocationListBuilder, *, value: datetime.datetime
) -> x509.CertificateRevocationListBuilder:
return builder.last_update(value)
def set_revocation_date(
builder: x509.RevokedCertificateBuilder, *, value: datetime.datetime
) -> x509.RevokedCertificateBuilder:
return builder.revocation_date(value)
__all__ = (
"REVOCATION_REASON_MAP",
"REVOCATION_REASON_MAP_INVERSE",
"cryptography_decode_revoked_certificate",
"cryptography_dump_revoked",
"cryptography_get_signature_algorithm_oid_from_crl",
"get_next_update",
"get_last_update",
"get_revocation_date",
"get_invalidity_date",
"set_next_update",
"set_last_update",
"set_revocation_date",
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,170 @@
# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
def binary_exp_mod(f: int, e: int, *, m: int) -> int:
"""Computes f^e mod m in O(log e) multiplications modulo m."""
# Compute len_e = floor(log_2(e))
len_e = -1
x = e
while x > 0:
x >>= 1
len_e += 1
# Compute f**e mod m
result = 1
for k in range(len_e, -1, -1):
result = (result * result) % m
if ((e >> k) & 1) != 0:
result = (result * f) % m
return result
def simple_gcd(a: int, b: int) -> int:
"""Compute GCD of its two inputs."""
while b != 0:
a, b = b, a % b
return a
def quick_is_not_prime(n: int) -> bool:
"""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
that we could not detect quickly whether it is not prime.
"""
if n <= 2:
return n < 2
# The constant in the next line is the product of all primes < 200
prime_product = 7799922041683461553249199106329813876687996789903550945093032474868511536164700810
gcd = simple_gcd(n, prime_product)
if gcd > 1:
if n < 200 and gcd == n:
# Explicitly check for all primes < 200
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,
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
# TODO: maybe do some iterations of Miller-Rabin to increase confidence
# (https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test)
return False
def count_bytes(no: int) -> int:
"""
Given an integer, compute the number of bytes necessary to store its absolute value.
"""
no = abs(no)
if no == 0:
return 0
return (no.bit_length() + 7) // 8
def count_bits(no: int) -> int:
"""
Given an integer, compute the number of bits necessary to store its absolute value.
"""
no = abs(no)
if no == 0:
return 0
return no.bit_length()
def convert_int_to_bytes(no: int, *, count: int | None = None) -> bytes:
"""
Convert the absolute value of an integer to a byte string in network byte order.
If ``count`` is provided, it must be sufficiently large so that the integer's
absolute value can be represented with these number of bytes. The resulting byte
string will have length exactly ``count``.
The value zero will be converted to an empty byte string if ``count`` is provided.
"""
no = abs(no)
if count is None:
count = count_bytes(no)
return no.to_bytes(count, byteorder="big")
def convert_int_to_hex(no: int, *, digits: int | None = None) -> str:
"""
Convert the absolute value of an integer to a string of hexadecimal digits.
If ``digits`` is provided, the string will be padded on the left with ``0``s so
that the returned value has length ``digits``. If ``digits`` is not sufficient,
the string will be longer.
"""
no = abs(no)
value = f"{no:x}"
if digits is not None and len(value) < digits:
value = "0" * (digits - len(value)) + value
return value
def convert_bytes_to_int(data: bytes) -> int:
"""
Convert a byte string to an unsigned integer in network byte order.
"""
return int.from_bytes(data, byteorder="big", signed=False)
__all__ = (
"binary_exp_mod",
"simple_gcd",
"quick_is_not_prime",
"count_bytes",
"count_bits",
"convert_int_to_bytes",
"convert_int_to_hex",
"convert_bytes_to_int",
)

View File

@@ -0,0 +1,417 @@
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# GNU General Public License v3.0+ (see 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import abc
import typing as t
from ansible_collections.community.crypto.plugins.module_utils._argspec import (
ArgumentSpec,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
OpenSSLBadPassphraseError,
OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
cryptography_compare_public_keys,
get_not_valid_after,
get_not_valid_before,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.certificate_info import (
get_certificate_info,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.support import (
load_certificate,
load_certificate_privatekey,
load_certificate_request,
)
from ansible_collections.community.crypto.plugins.module_utils._cryptography_dep import (
COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION,
assert_required_cryptography_version,
)
if t.TYPE_CHECKING:
import datetime
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
CertificatePrivateKeyTypes,
)
from cryptography.hazmat.primitives.asymmetric.types import (
CertificateIssuerPrivateKeyTypes,
)
MINIMAL_CRYPTOGRAPHY_VERSION = COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION
try:
import cryptography
from cryptography import x509
except ImportError:
pass
class CertificateError(OpenSSLObjectError):
pass
class CertificateBackend(metaclass=abc.ABCMeta):
def __init__(self, *, module: AnsibleModule) -> None:
self.module = module
self.force: bool = module.params["force"]
self.ignore_timestamps: bool = module.params["ignore_timestamps"]
self.privatekey_path: str | None = module.params["privatekey_path"]
privatekey_content: str | None = module.params["privatekey_content"]
if privatekey_content is not None:
self.privatekey_content: bytes | None = privatekey_content.encode("utf-8")
else:
self.privatekey_content = None
self.privatekey_passphrase: str | None = module.params["privatekey_passphrase"]
self.csr_path: str | None = module.params["csr_path"]
csr_content = module.params["csr_content"]
if csr_content is not None:
self.csr_content: bytes | None = csr_content.encode("utf-8")
else:
self.csr_content = None
# The following are default values which make sure check() works as
# before if providers do not explicitly change these properties.
self.create_subject_key_identifier: str = "never_create"
self.create_authority_key_identifier: bool = False
self.privatekey: CertificatePrivateKeyTypes | None = None
self.csr: x509.CertificateSigningRequest | None = None
self.cert: x509.Certificate | None = None
self.existing_certificate: x509.Certificate | None = None
self.existing_certificate_bytes: bytes | None = None
self.check_csr_subject: bool = True
self.check_csr_extensions: bool = True
self.diff_before = self._get_info(None)
self.diff_after = self._get_info(None)
def _get_info(self, data: bytes | None) -> dict[str, t.Any]:
if data is None:
return {}
try:
result = get_certificate_info(
module=self.module, content=data, prefer_one_fingerprint=True
)
result["can_parse_certificate"] = True
return result
except Exception:
return {"can_parse_certificate": False}
@abc.abstractmethod
def generate_certificate(self) -> None:
"""(Re-)Generate certificate."""
@abc.abstractmethod
def get_certificate_data(self) -> bytes:
"""Return bytes for self.cert."""
def set_existing(self, certificate_bytes: bytes | None) -> None:
"""Set existing certificate bytes. None indicates that the key does not exist."""
self.existing_certificate_bytes = certificate_bytes
self.diff_after = self.diff_before = self._get_info(
self.existing_certificate_bytes
)
def has_existing(self) -> bool:
"""Query whether an existing certificate is/has been there."""
return self.existing_certificate_bytes is not None
def _ensure_private_key_loaded(self) -> None:
"""Load the provided private key into self.privatekey."""
if self.privatekey is not None:
return
if self.privatekey_path is None and self.privatekey_content is None:
return
try:
self.privatekey = load_certificate_privatekey(
path=self.privatekey_path,
content=self.privatekey_content,
passphrase=self.privatekey_passphrase,
)
except OpenSSLBadPassphraseError as exc:
raise CertificateError(exc) from exc
def _ensure_csr_loaded(self) -> None:
"""Load the CSR into self.csr."""
if self.csr is not None:
return
if self.csr_path is None and self.csr_content is None:
return
self.csr = load_certificate_request(
path=self.csr_path,
content=self.csr_content,
)
def _ensure_existing_certificate_loaded(self) -> None:
"""Load the existing certificate into self.existing_certificate."""
if self.existing_certificate is not None:
return
if self.existing_certificate_bytes is None:
return
self.existing_certificate = load_certificate(
path=None,
content=self.existing_certificate_bytes,
)
def _check_privatekey(self) -> bool:
"""Check whether provided parameters match, assuming self.existing_certificate and self.privatekey have been populated."""
if self.existing_certificate is None:
raise AssertionError(
"Contract violation: existing_certificate has not been populated"
)
if self.privatekey is None:
raise AssertionError(
"Contract violation: privatekey has not been populated"
)
return cryptography_compare_public_keys(
self.existing_certificate.public_key(), self.privatekey.public_key()
)
def _check_csr(self) -> bool:
"""Check whether provided parameters match, assuming self.existing_certificate and self.csr have been populated."""
if self.existing_certificate is None:
raise AssertionError(
"Contract violation: existing_certificate has not been populated"
)
if self.csr is None:
raise AssertionError("Contract violation: csr has not been populated")
# Verify that CSR is signed by certificate's private key
if not self.csr.is_signature_valid:
return False
if not cryptography_compare_public_keys(
self.csr.public_key(), self.existing_certificate.public_key()
):
return False
# Check subject
if (
self.check_csr_subject
and self.csr.subject != self.existing_certificate.subject
):
return False
# Check extensions
if not self.check_csr_extensions:
return True
cert_exts = list(self.existing_certificate.extensions)
csr_exts = list(self.csr.extensions)
if self.create_subject_key_identifier != "never_create":
# Filter out SubjectKeyIdentifier extension before comparison
cert_exts = list(
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:
# Filter out AuthorityKeyIdentifier extension before comparison
cert_exts = list(
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):
return False
for cert_ext in cert_exts:
try:
csr_ext = self.csr.extensions.get_extension_for_oid(cert_ext.oid)
if cert_ext != csr_ext:
return False
except cryptography.x509.ExtensionNotFound:
return False
return True
def _check_subject_key_identifier(self) -> bool:
"""Check whether Subject Key Identifier matches, assuming self.existing_certificate and self.csr have been populated."""
if self.existing_certificate is None:
raise AssertionError(
"Contract violation: existing_certificate has not been populated"
)
if self.csr is None:
raise AssertionError("Contract violation: csr has not been populated")
# Get hold of certificate's SKI
try:
ext = self.existing_certificate.extensions.get_extension_for_class(
x509.SubjectKeyIdentifier
)
except cryptography.x509.ExtensionNotFound:
return False
# Get hold of CSR's SKI for 'create_if_not_provided'
csr_ext = None
if self.create_subject_key_identifier == "create_if_not_provided":
try:
csr_ext = self.csr.extensions.get_extension_for_class(
x509.SubjectKeyIdentifier
)
except cryptography.x509.ExtensionNotFound:
pass
if csr_ext is None:
# 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
):
return False
else:
# If CSR had SKI and we did not ignore it ('create_if_not_provided'), compare SKIs
if ext.value.digest != csr_ext.value.digest:
return False
return True
def needs_regeneration(
self,
*,
not_before: datetime.datetime | None = None,
not_after: datetime.datetime | None = None,
) -> bool:
"""Check whether a regeneration is necessary."""
if self.force or self.existing_certificate_bytes is None:
return True
try:
self._ensure_existing_certificate_loaded()
except Exception:
return True
assert self.existing_certificate is not None
# Check whether private key matches
self._ensure_private_key_loaded()
if self.privatekey is not None and not self._check_privatekey():
return True
# Check whether CSR matches
self._ensure_csr_loaded()
if self.csr is not None and not self._check_csr():
return True
# Check SubjectKeyIdentifier
if (
self.create_subject_key_identifier != "never_create"
and not self._check_subject_key_identifier()
):
return True
# Check not before
if not_before is not None and not self.ignore_timestamps:
if get_not_valid_before(self.existing_certificate) != not_before:
return True
# Check not after
if not_after is not None and not self.ignore_timestamps:
if get_not_valid_after(self.existing_certificate) != not_after:
return True
return False
def dump(self, *, include_certificate: bool) -> dict[str, t.Any]:
"""Serialize the object into a dictionary."""
result: dict[str, t.Any] = {
"privatekey": self.privatekey_path,
"csr": self.csr_path,
}
# Get hold of certificate bytes
certificate_bytes = self.existing_certificate_bytes
if self.cert is not None:
certificate_bytes = self.get_certificate_data()
self.diff_after = self._get_info(certificate_bytes)
if include_certificate:
# Store result
result["certificate"] = (
certificate_bytes.decode("utf-8") if certificate_bytes else None
)
result["diff"] = {
"before": self.diff_before,
"after": self.diff_after,
}
return result
class CertificateProvider(metaclass=abc.ABCMeta):
@abc.abstractmethod
def validate_module_args(self, module: AnsibleModule) -> None:
"""Check module arguments"""
@abc.abstractmethod
def create_backend(self, module: AnsibleModule) -> CertificateBackend:
"""Create an implementation for a backend.
Return value must be instance of CertificateBackend.
"""
def select_backend(
*, module: AnsibleModule, provider: CertificateProvider
) -> CertificateBackend:
provider.validate_module_args(module)
assert_required_cryptography_version(
module, minimum_cryptography_version=MINIMAL_CRYPTOGRAPHY_VERSION
)
return provider.create_backend(module)
def get_certificate_argument_spec() -> ArgumentSpec:
return ArgumentSpec(
argument_spec={
"provider": {
"type": "str",
"choices": [],
}, # choices will be filled by add_XXX_provider_to_argument_spec() in certificate_xxx.py
"force": {
"type": "bool",
"default": False,
},
"csr_path": {"type": "path"},
"csr_content": {"type": "str"},
"ignore_timestamps": {"type": "bool", "default": True},
"select_crypto_backend": {
"type": "str",
"default": "auto",
"choices": ["auto", "cryptography"],
},
# General properties of a certificate
"privatekey_path": {"type": "path"},
"privatekey_content": {"type": "str", "no_log": True},
"privatekey_passphrase": {"type": "str", "no_log": True},
},
mutually_exclusive=[
["csr_path", "csr_content"],
["privatekey_path", "privatekey_content"],
],
)
__all__ = (
"CertificateError",
"CertificateBackend",
"CertificateProvider",
"get_certificate_argument_spec",
)

View File

@@ -0,0 +1,145 @@
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# GNU General Public License v3.0+ (see 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import os
import tempfile
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.certificate import (
CertificateBackend,
CertificateError,
CertificateProvider,
)
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._argspec import (
ArgumentSpec,
)
class AcmeCertificateBackend(CertificateBackend):
def __init__(self, *, module: AnsibleModule) -> None:
super().__init__(module=module)
self.accountkey_path: str = module.params["acme_accountkey_path"]
self.challenge_path: str = module.params["acme_challenge_path"]
self.use_chain: bool = module.params["acme_chain"]
self.acme_directory: str = module.params["acme_directory"]
self.cert_bytes: bytes | None = None
if self.csr_content is None:
if self.csr_path is None:
raise CertificateError(
"csr_path or csr_content is required for ownca provider"
)
if not os.path.exists(self.csr_path):
raise CertificateError(
f"The certificate signing request file {self.csr_path} does not exist"
)
if not os.path.exists(self.accountkey_path):
raise CertificateError(
f"The account key {self.accountkey_path} does not exist"
)
if not os.path.exists(self.challenge_path):
raise CertificateError(
f"The challenge path {self.challenge_path} does not exist"
)
self.acme_tiny_path = self.module.get_bin_path("acme-tiny", required=True)
def generate_certificate(self) -> None:
"""(Re-)Generate certificate."""
command = [self.acme_tiny_path]
if self.use_chain:
command.append("--chain")
command.extend(["--account-key", self.accountkey_path])
if self.csr_content is not None:
# We need to temporarily write the CSR to disk
fd, tmpsrc = tempfile.mkstemp()
self.module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
f = os.fdopen(fd, "wb")
try:
f.write(self.csr_content)
except Exception as err:
try:
f.close()
except Exception:
pass
self.module.fail_json(
msg=f"failed to create temporary CSR file: {err}",
exception=traceback.format_exc(),
)
f.close()
command.extend(["--csr", tmpsrc])
else:
command.extend(["--csr", self.csr_path])
command.extend(["--acme-dir", self.challenge_path])
command.extend(["--directory-url", self.acme_directory])
try:
self.cert_bytes = to_bytes(
self.module.run_command(command, check_rc=True)[1]
)
except OSError as exc:
raise CertificateError(exc) from exc
def get_certificate_data(self) -> bytes:
"""Return bytes for self.cert."""
if self.cert_bytes is None:
raise AssertionError("Contract violation: cert_bytes is None")
return self.cert_bytes
def dump(self, *, include_certificate: bool) -> dict[str, t.Any]:
result = super().dump(include_certificate=include_certificate)
result["accountkey"] = self.accountkey_path
return result
class AcmeCertificateProvider(CertificateProvider):
def validate_module_args(self, module: AnsibleModule) -> 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."
)
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 create_backend(self, module: AnsibleModule) -> AcmeCertificateBackend:
return AcmeCertificateBackend(module=module)
def add_acme_provider_to_argument_spec(argument_spec: ArgumentSpec) -> None:
argument_spec.argument_spec["provider"]["choices"].append("acme")
argument_spec.argument_spec.update(
{
"acme_accountkey_path": {"type": "path"},
"acme_challenge_path": {"type": "path"},
"acme_chain": {"type": "bool", "default": False},
"acme_directory": {
"type": "str",
"default": "https://acme-v02.api.letsencrypt.org/directory",
},
}
)
__all__ = (
"AcmeCertificateBackend",
"AcmeCertificateProvider",
"add_acme_provider_to_argument_spec",
)

View File

@@ -0,0 +1,297 @@
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# GNU General Public License v3.0+ (see 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import datetime
import os
import typing as t
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
get_not_valid_after,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.certificate import (
CertificateBackend,
CertificateError,
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 (
get_now_datetime,
get_relative_time_option,
)
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._argspec import (
ArgumentSpec,
)
try:
from cryptography.x509.oid import NameOID
except ImportError:
pass
class EntrustCertificateBackend(CertificateBackend):
def __init__(self, *, module: AnsibleModule) -> None:
super().__init__(module=module)
self.trackingId = None
self.notAfter = get_relative_time_option(
module.params["entrust_not_after"],
input_name="entrust_not_after",
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
self.cert_bytes: bytes | None = None
if self.csr_content is None:
if self.csr_path is None:
raise CertificateError(
"csr_path or csr_content is required for entrust provider"
)
if not os.path.exists(self.csr_path):
raise CertificateError(
f"The certificate signing request file {self.csr_path} does not exist"
)
self._ensure_csr_loaded()
if self.csr is None:
raise CertificateError("CSR not provided")
# ECS API defaults to using the validated organization tied to the account.
# We want to always force behavior of trying to use the organization provided in the CSR.
# To that end we need to parse out the organization from the CSR.
self.csr_org = None
csr_subject_orgs = self.csr.subject.get_attributes_for_oid(
NameOID.ORGANIZATION_NAME
)
if len(csr_subject_orgs) == 1:
self.csr_org = csr_subject_orgs[0].value
elif len(csr_subject_orgs) > 1:
self.module.fail_json(
msg=(
"Entrust provider does not currently support multiple validated organizations. Multiple organizations found in "
f"Subject DN: '{self.csr.subject}'. "
)
)
# 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.
if self.csr_org is None:
self.csr_org = ""
try:
self.ecs_client = ECSClient(
entrust_api_user=self.module.params["entrust_api_user"],
entrust_api_key=self.module.params["entrust_api_key"],
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_specification_path=self.module.params[
"entrust_api_specification_path"
],
)
except SessionConfigurationException as e:
module.fail_json(msg=f"Failed to initialize Entrust Provider: {e}")
def generate_certificate(self) -> None:
"""(Re-)Generate certificate."""
body = {}
# Read the CSR that was generated for us
if self.csr_content is not None:
# csr_content contains bytes
body["csr"] = to_text(self.csr_content)
else:
assert self.csr_path is not None
with open(self.csr_path, "r", encoding="utf-8") as csr_file:
body["csr"] = csr_file.read()
body["certType"] = self.module.params["entrust_cert_type"]
# Handle expiration (30 days if not specified)
expiry = self.notAfter
if not expiry:
gmt_now = get_now_datetime(with_timezone=CRYPTOGRAPHY_TIMEZONE)
expiry = gmt_now + datetime.timedelta(days=365)
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
body["certExpiryDate"] = expiry_iso3339
body["org"] = self.csr_org
body["tracking"] = {
"requesterName": self.module.params["entrust_requester_name"],
"requesterEmail": self.module.params["entrust_requester_email"],
"requesterPhone": self.module.params["entrust_requester_phone"],
}
try:
result = self.ecs_client.NewCertRequest( # type: ignore[attr-defined] # pylint: disable=no-member
Body=body
)
self.trackingId = result.get("trackingId")
except RestOperationException as e:
self.module.fail_json(
msg=f"Failed to request new certificate from Entrust Certificate Services (ECS): {e.message}"
)
self.cert_bytes = to_bytes(result.get("endEntityCert"))
self.cert = load_certificate(
path=None,
content=self.cert_bytes,
)
def get_certificate_data(self) -> bytes:
"""Return bytes for self.cert."""
if self.cert_bytes is None:
raise AssertionError("Contract violation: cert_bytes not set")
return self.cert_bytes
def needs_regeneration(
self,
*,
not_before: datetime.datetime | None = None,
not_after: datetime.datetime | None = None,
) -> bool:
parent_check = super().needs_regeneration()
try:
cert_details = self._get_cert_details()
except RestOperationException as e:
self.module.fail_json(
msg=f"Failed to get status of existing certificate from Entrust Certificate Services (ECS): {e.message}."
)
# Always issue a new certificate if the certificate is expired, suspended or revoked
status = cert_details.get("status", False)
if status in ("EXPIRED", "SUSPENDED", "REVOKED"):
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 (
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 parent_check
def _get_cert_details(self) -> dict[str, t.Any]:
cert_details: dict[str, t.Any] = {}
try:
self._ensure_existing_certificate_loaded()
except Exception:
return cert_details
if self.existing_certificate:
serial_number = f"{self.existing_certificate.serial_number:X}"
expiry = get_not_valid_after(self.existing_certificate)
# get some information about the expiry of this certificate
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
cert_details["expiresAfter"] = expiry_iso3339
# If a trackingId is not already defined (from the result of a generate)
# use the serial number to identify the tracking Id
if self.trackingId is None and serial_number is not None:
cert_results = self.ecs_client.GetCertificates( # type: ignore[attr-defined] # pylint: disable=no-member
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
# 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.
if len(cert_results) == 1:
self.trackingId = cert_results[0].get("trackingId")
if self.trackingId is not None:
cert_details.update(
self.ecs_client.GetCertificate( # pylint: disable=no-member
trackingId=self.trackingId
)
)
return cert_details
class EntrustCertificateProvider(CertificateProvider):
def validate_module_args(self, module: AnsibleModule) -> None:
pass
def create_backend(self, module: AnsibleModule) -> EntrustCertificateBackend:
return EntrustCertificateBackend(module=module)
def add_entrust_provider_to_argument_spec(argument_spec: ArgumentSpec) -> None:
argument_spec.argument_spec["provider"]["choices"].append("entrust")
argument_spec.argument_spec.update(
{
"entrust_cert_type": {
"type": "str",
"default": "STANDARD_SSL",
"choices": [
"STANDARD_SSL",
"ADVANTAGE_SSL",
"UC_SSL",
"EV_SSL",
"WILDCARD_SSL",
"PRIVATE_SSL",
"PD_SSL",
"CDS_ENT_LITE",
"CDS_ENT_PRO",
"SMIME_ENT",
],
},
"entrust_requester_email": {"type": "str"},
"entrust_requester_name": {"type": "str"},
"entrust_requester_phone": {"type": "str"},
"entrust_api_user": {"type": "str"},
"entrust_api_key": {"type": "str", "no_log": True},
"entrust_api_client_cert_path": {"type": "path"},
"entrust_api_client_cert_key_path": {"type": "path", "no_log": True},
"entrust_api_specification_path": {
"type": "path",
"default": "https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml",
},
"entrust_not_after": {"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",
],
)
)
__all__ = (
"EntrustCertificateBackend",
"EntrustCertificateProvider",
"add_entrust_provider_to_argument_spec",
)

View File

@@ -0,0 +1,487 @@
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import abc
import binascii
import typing as t
from ansible.module_utils.common.text.converters import to_text
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_decode_name,
cryptography_get_extensions_from_cert,
cryptography_oid_to_name,
get_not_valid_after,
get_not_valid_before,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.publickey_info import (
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._cryptography_dep import (
COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION,
assert_required_cryptography_version,
)
from ansible_collections.community.crypto.plugins.module_utils._time import (
get_now_datetime,
)
if t.TYPE_CHECKING:
import datetime
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._argspec import (
ArgumentSpec,
)
from ansible_collections.community.crypto.plugins.plugin_utils._action_module import (
AnsibleActionModule,
)
from ansible_collections.community.crypto.plugins.plugin_utils._filter_module import (
FilterModuleMock,
)
from cryptography.hazmat.primitives.asymmetric.types import PublicKeyTypes
GeneralAnsibleModule = t.Union[AnsibleModule, AnsibleActionModule, FilterModuleMock]
MINIMAL_CRYPTOGRAPHY_VERSION = COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION
try:
import cryptography
from cryptography import x509
from cryptography.hazmat.primitives import serialization
except ImportError:
pass
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
class CertificateInfoRetrieval(metaclass=abc.ABCMeta):
cert: x509.Certificate
def __init__(self, *, module: GeneralAnsibleModule, content: bytes) -> None:
# content must be a bytes string
self.module = module
self.content = content
@abc.abstractmethod
def _get_der_bytes(self) -> bytes:
pass
@abc.abstractmethod
def _get_signature_algorithm(self) -> str:
pass
@abc.abstractmethod
def _get_subject_ordered(self) -> list[list[str]]:
pass
@abc.abstractmethod
def _get_issuer_ordered(self) -> list[list[str]]:
pass
@abc.abstractmethod
def _get_version(self) -> int | str:
pass
@abc.abstractmethod
def _get_key_usage(self) -> tuple[list[str] | None, bool]:
pass
@abc.abstractmethod
def _get_extended_key_usage(self) -> tuple[list[str] | None, bool]:
pass
@abc.abstractmethod
def _get_basic_constraints(self) -> tuple[list[str] | None, bool]:
pass
@abc.abstractmethod
def _get_ocsp_must_staple(self) -> tuple[bool | None, bool]:
pass
@abc.abstractmethod
def _get_subject_alt_name(self) -> tuple[list[str] | None, bool]:
pass
@abc.abstractmethod
def get_not_before(self) -> datetime.datetime:
pass
@abc.abstractmethod
def get_not_after(self) -> datetime.datetime:
pass
@abc.abstractmethod
def _get_public_key_pem(self) -> bytes:
pass
@abc.abstractmethod
def _get_public_key_object(self) -> PublicKeyTypes:
pass
@abc.abstractmethod
def _get_subject_key_identifier(self) -> bytes | None:
pass
@abc.abstractmethod
def _get_authority_key_identifier(
self,
) -> tuple[bytes | None, list[str] | None, int | None]:
pass
@abc.abstractmethod
def _get_serial_number(self) -> int:
pass
@abc.abstractmethod
def _get_all_extensions(self) -> dict[str, dict[str, bool | str]]:
pass
@abc.abstractmethod
def _get_ocsp_uri(self) -> str | None:
pass
@abc.abstractmethod
def _get_issuer_uri(self) -> str | None:
pass
def get_info(
self, *, prefer_one_fingerprint: bool = False, der_support_enabled: bool = False
) -> dict[str, t.Any]:
result: dict[str, t.Any] = {}
self.cert = load_certificate(
content=self.content,
der_support_enabled=der_support_enabled,
)
result["signature_algorithm"] = self._get_signature_algorithm()
subject = self._get_subject_ordered()
issuer = self._get_issuer_ordered()
result["subject"] = {}
for k, v in subject:
result["subject"][k] = v
result["subject_ordered"] = subject
result["issuer"] = {}
for k, v in issuer:
result["issuer"][k] = v
result["issuer_ordered"] = issuer
result["version"] = self._get_version()
result["key_usage"], result["key_usage_critical"] = self._get_key_usage()
result["extended_key_usage"], result["extended_key_usage_critical"] = (
self._get_extended_key_usage()
)
result["basic_constraints"], result["basic_constraints_critical"] = (
self._get_basic_constraints()
)
result["ocsp_must_staple"], result["ocsp_must_staple_critical"] = (
self._get_ocsp_must_staple()
)
result["subject_alt_name"], result["subject_alt_name_critical"] = (
self._get_subject_alt_name()
)
not_before = self.get_not_before()
not_after = self.get_not_after()
result["not_before"] = not_before.strftime(TIMESTAMP_FORMAT)
result["not_after"] = not_after.strftime(TIMESTAMP_FORMAT)
result["expired"] = not_after < get_now_datetime(
with_timezone=CRYPTOGRAPHY_TIMEZONE
)
result["public_key"] = to_text(self._get_public_key_pem())
public_key_info = get_publickey_info(
module=self.module,
key=self._get_public_key_object(),
prefer_one_fingerprint=prefer_one_fingerprint,
)
result.update(
{
"public_key_type": public_key_info["type"],
"public_key_data": public_key_info["public_data"],
"public_key_fingerprints": public_key_info["fingerprints"],
}
)
result["fingerprints"] = get_fingerprint_of_bytes(
self._get_der_bytes(), prefer_one=prefer_one_fingerprint
)
ski_bytes = self._get_subject_key_identifier()
if ski_bytes is not None:
ski = binascii.hexlify(ski_bytes).decode("ascii")
ski = ":".join([ski[i : i + 2] for i in range(0, len(ski), 2)])
else:
ski = None
result["subject_key_identifier"] = ski
aki_bytes, aci, acsn = self._get_authority_key_identifier()
if aki_bytes is not None:
aki = binascii.hexlify(aki_bytes).decode("ascii")
aki = ":".join([aki[i : i + 2] for i in range(0, len(aki), 2)])
else:
aki = None
result["authority_key_identifier"] = aki
result["authority_cert_issuer"] = aci
result["authority_cert_serial_number"] = acsn
result["serial_number"] = self._get_serial_number()
result["extensions_by_oid"] = self._get_all_extensions()
result["ocsp_uri"] = self._get_ocsp_uri()
result["issuer_uri"] = self._get_issuer_uri()
return result
class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
"""Validate the supplied cert, using the cryptography backend"""
def __init__(self, *, module: GeneralAnsibleModule, content: bytes) -> None:
super().__init__(module=module, content=content)
self.name_encoding = module.params.get("name_encoding", "ignore")
def _get_der_bytes(self) -> bytes:
return self.cert.public_bytes(serialization.Encoding.DER)
def _get_signature_algorithm(self) -> str:
return cryptography_oid_to_name(self.cert.signature_algorithm_oid)
def _get_subject_ordered(self) -> list[list[str]]:
result: list[list[str]] = []
for attribute in self.cert.subject:
result.append(
[cryptography_oid_to_name(attribute.oid), to_text(attribute.value)]
)
return result
def _get_issuer_ordered(self) -> list[list[str]]:
result = []
for attribute in self.cert.issuer:
result.append(
[cryptography_oid_to_name(attribute.oid), to_text(attribute.value)]
)
return result
def _get_version(self) -> int | str:
if self.cert.version == x509.Version.v1:
return 1
if self.cert.version == x509.Version.v3:
return 3
return "unknown"
def _get_key_usage(self) -> tuple[list[str] | None, bool]:
try:
current_key_ext = self.cert.extensions.get_extension_for_class(
x509.KeyUsage
)
current_key_usage = current_key_ext.value
key_usage = {
"digital_signature": current_key_usage.digital_signature,
"content_commitment": current_key_usage.content_commitment,
"key_encipherment": current_key_usage.key_encipherment,
"data_encipherment": current_key_usage.data_encipherment,
"key_agreement": current_key_usage.key_agreement,
"key_cert_sign": current_key_usage.key_cert_sign,
"crl_sign": current_key_usage.crl_sign,
"encipher_only": False,
"decipher_only": False,
}
if key_usage["key_agreement"]:
key_usage.update(
{
"encipher_only": current_key_usage.encipher_only,
"decipher_only": current_key_usage.decipher_only,
}
)
key_usage_names = {
"digital_signature": "Digital Signature",
"content_commitment": "Non Repudiation",
"key_encipherment": "Key Encipherment",
"data_encipherment": "Data Encipherment",
"key_agreement": "Key Agreement",
"key_cert_sign": "Certificate Sign",
"crl_sign": "CRL Sign",
"encipher_only": "Encipher Only",
"decipher_only": "Decipher Only",
}
return (
sorted(
[
key_usage_names[name]
for name, value in key_usage.items()
if value
]
),
current_key_ext.critical,
)
except cryptography.x509.ExtensionNotFound:
return None, False
def _get_extended_key_usage(self) -> tuple[list[str] | None, bool]:
try:
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(
x509.ExtendedKeyUsage
)
return (
sorted(
[cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value]
),
ext_keyusage_ext.critical,
)
except cryptography.x509.ExtensionNotFound:
return None, False
def _get_basic_constraints(self) -> tuple[list[str] | None, bool]:
try:
ext_keyusage_ext = self.cert.extensions.get_extension_for_class(
x509.BasicConstraints
)
result = []
result.append(f"CA:{'TRUE' if ext_keyusage_ext.value.ca else 'FALSE'}")
if ext_keyusage_ext.value.path_length is not None:
result.append(f"pathlen:{ext_keyusage_ext.value.path_length}")
return sorted(result), ext_keyusage_ext.critical
except cryptography.x509.ExtensionNotFound:
return None, False
def _get_ocsp_must_staple(self) -> tuple[bool | None, bool]:
try:
tlsfeature_ext = self.cert.extensions.get_extension_for_class(
x509.TLSFeature
)
value = (
cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
)
return value, tlsfeature_ext.critical
except cryptography.x509.ExtensionNotFound:
return None, False
def _get_subject_alt_name(self) -> tuple[list[str] | None, bool]:
try:
san_ext = self.cert.extensions.get_extension_for_class(
x509.SubjectAlternativeName
)
result = [
cryptography_decode_name(san, idn_rewrite=self.name_encoding)
for san in san_ext.value
]
return result, san_ext.critical
except cryptography.x509.ExtensionNotFound:
return None, False
def get_not_before(self) -> datetime.datetime:
return get_not_valid_before(self.cert)
def get_not_after(self) -> datetime.datetime:
return get_not_valid_after(self.cert)
def _get_public_key_pem(self) -> bytes:
return self.cert.public_key().public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo,
)
def _get_public_key_object(self) -> PublicKeyTypes:
return self.cert.public_key()
def _get_subject_key_identifier(self) -> bytes | None:
try:
ext = self.cert.extensions.get_extension_for_class(
x509.SubjectKeyIdentifier
)
return ext.value.digest
except cryptography.x509.ExtensionNotFound:
return None
def _get_authority_key_identifier(
self,
) -> tuple[bytes | None, list[str] | None, int | None]:
try:
ext = self.cert.extensions.get_extension_for_class(
x509.AuthorityKeyIdentifier
)
issuer = None
if ext.value.authority_cert_issuer is not None:
issuer = [
cryptography_decode_name(san, 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:
return None, None, None
def _get_serial_number(self) -> int:
return self.cert.serial_number
def _get_all_extensions(self) -> dict[str, dict[str, bool | str]]:
return cryptography_get_extensions_from_cert(self.cert)
def _get_ocsp_uri(self) -> str | None:
try:
ext = self.cert.extensions.get_extension_for_class(
x509.AuthorityInformationAccess
)
for desc in ext.value:
if desc.access_method == x509.oid.AuthorityInformationAccessOID.OCSP:
if isinstance(desc.access_location, x509.UniformResourceIdentifier):
return desc.access_location.value
except x509.ExtensionNotFound:
pass
return None
def _get_issuer_uri(self) -> str | None:
try:
ext = self.cert.extensions.get_extension_for_class(
x509.AuthorityInformationAccess
)
for desc in ext.value:
if (
desc.access_method
== x509.oid.AuthorityInformationAccessOID.CA_ISSUERS
):
if isinstance(desc.access_location, x509.UniformResourceIdentifier):
return desc.access_location.value
except x509.ExtensionNotFound:
pass
return None
def get_certificate_info(
*,
module: GeneralAnsibleModule,
content: bytes,
prefer_one_fingerprint: bool = False,
) -> dict[str, t.Any]:
info = CertificateInfoRetrievalCryptography(module=module, content=content)
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
def select_backend(
*, module: GeneralAnsibleModule, content: bytes
) -> CertificateInfoRetrieval:
assert_required_cryptography_version(
module, minimum_cryptography_version=MINIMAL_CRYPTOGRAPHY_VERSION
)
return CertificateInfoRetrievalCryptography(module=module, content=content)
__all__ = ("CertificateInfoRetrieval", "get_certificate_info", "select_backend")

View File

@@ -0,0 +1,374 @@
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# GNU General Public License v3.0+ (see 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import os
import typing as t
from random import randrange
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
OpenSSLBadPassphraseError,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_compare_public_keys,
cryptography_key_needs_digest_for_signing,
cryptography_verify_certificate_signature,
get_not_valid_after,
get_not_valid_before,
is_potential_certificate_issuer_public_key,
set_not_valid_after,
set_not_valid_before,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.certificate import (
CertificateBackend,
CertificateError,
CertificateProvider,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.support import (
load_certificate,
load_certificate_issuer_privatekey,
select_message_digest,
)
from ansible_collections.community.crypto.plugins.module_utils._time import (
get_relative_time_option,
)
if t.TYPE_CHECKING:
import datetime
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._argspec import (
ArgumentSpec,
)
from cryptography.hazmat.primitives.asymmetric.types import (
CertificateIssuerPrivateKeyTypes,
)
try:
import cryptography
from cryptography import x509
from cryptography.hazmat.primitives.serialization import Encoding
except ImportError:
pass
class OwnCACertificateBackendCryptography(CertificateBackend):
def __init__(self, *, module: AnsibleModule) -> None:
super().__init__(module=module)
self.create_subject_key_identifier: t.Literal[
"create_if_not_provided", "always_create", "never_create"
] = module.params["ownca_create_subject_key_identifier"]
self.create_authority_key_identifier: bool = module.params[
"ownca_create_authority_key_identifier"
]
self.notBefore = get_relative_time_option(
module.params["ownca_not_before"],
input_name="ownca_not_before",
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
self.notAfter = get_relative_time_option(
module.params["ownca_not_after"],
input_name="ownca_not_after",
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
self.digest = select_message_digest(module.params["ownca_digest"])
self.serial_number = x509.random_serial_number()
self.ca_cert_path: str | None = module.params["ownca_path"]
ca_cert_content: str | None = module.params["ownca_content"]
if ca_cert_content is not None:
self.ca_cert_content: bytes | None = ca_cert_content.encode("utf-8")
else:
self.ca_cert_content = None
self.ca_privatekey_path: str | None = module.params["ownca_privatekey_path"]
ca_privatekey_content: str | None = module.params["ownca_privatekey_content"]
if ca_privatekey_content is not None:
self.ca_privatekey_content: bytes | None = ca_privatekey_content.encode(
"utf-8"
)
else:
self.ca_privatekey_content = None
self.ca_privatekey_passphrase: str | None = module.params[
"ownca_privatekey_passphrase"
]
if self.csr_content is None:
if self.csr_path is None:
raise CertificateError(
"csr_path or csr_content is required for ownca provider"
)
if not os.path.exists(self.csr_path):
raise CertificateError(
f"The certificate signing request file {self.csr_path} does not exist"
)
if self.ca_cert_path is not None and not os.path.exists(self.ca_cert_path):
raise CertificateError(
f"The CA certificate file {self.ca_cert_path} does not exist"
)
if self.ca_privatekey_path is not None and not os.path.exists(
self.ca_privatekey_path
):
raise CertificateError(
f"The CA private key file {self.ca_privatekey_path} does not exist"
)
self._ensure_csr_loaded()
self.ca_cert = load_certificate(
path=self.ca_cert_path,
content=self.ca_cert_content,
)
if not is_potential_certificate_issuer_public_key(self.ca_cert.public_key()):
raise CertificateError(
"CA certificate's public key cannot be used to sign certificates"
)
try:
self.ca_private_key = load_certificate_issuer_privatekey(
path=self.ca_privatekey_path,
content=self.ca_privatekey_content,
passphrase=self.ca_privatekey_passphrase,
)
except OpenSSLBadPassphraseError as exc:
module.fail_json(msg=str(exc))
if not cryptography_compare_public_keys(
self.ca_cert.public_key(), self.ca_private_key.public_key()
):
raise CertificateError(
"The CA private key does not belong to the CA certificate"
)
if cryptography_key_needs_digest_for_signing(self.ca_private_key):
if self.digest is None:
raise CertificateError(
f"The digest {module.params['ownca_digest']} is not supported with the cryptography backend"
)
else:
self.digest = None
def generate_certificate(self) -> None:
"""(Re-)Generate certificate."""
if self.csr is None:
raise AssertionError("Contract violation: csr has not been populated")
cert_builder = x509.CertificateBuilder()
cert_builder = cert_builder.subject_name(self.csr.subject)
cert_builder = cert_builder.issuer_name(self.ca_cert.subject)
cert_builder = cert_builder.serial_number(self.serial_number)
cert_builder = set_not_valid_before(cert_builder, self.notBefore)
cert_builder = set_not_valid_after(cert_builder, self.notAfter)
cert_builder = cert_builder.public_key(self.csr.public_key())
has_ski = False
for extension in self.csr.extensions:
if isinstance(extension.value, x509.SubjectKeyIdentifier):
if self.create_subject_key_identifier == "always_create":
continue
has_ski = True
if self.create_authority_key_identifier and isinstance(
extension.value, x509.AuthorityKeyIdentifier
):
continue
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(
x509.SubjectKeyIdentifier.from_public_key(self.csr.public_key()),
critical=False,
)
if self.create_authority_key_identifier:
try:
ext = self.ca_cert.extensions.get_extension_for_class(
x509.SubjectKeyIdentifier
)
cert_builder = cert_builder.add_extension(
(
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
ext.value
)
),
critical=False,
)
except cryptography.x509.ExtensionNotFound:
public_key = self.ca_cert.public_key()
assert is_potential_certificate_issuer_public_key(public_key)
cert_builder = cert_builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(public_key),
critical=False,
)
certificate = cert_builder.sign(
private_key=self.ca_private_key,
algorithm=self.digest,
)
self.cert = certificate
def get_certificate_data(self) -> bytes:
"""Return bytes for self.cert."""
if self.cert is None:
raise AssertionError("Contract violation: cert has not been populated")
return self.cert.public_bytes(Encoding.PEM)
def needs_regeneration(
self,
*,
not_before: datetime.datetime | None = None,
not_after: datetime.datetime | None = None,
) -> bool:
if super().needs_regeneration(
not_before=self.notBefore, not_after=self.notAfter
):
return True
self._ensure_existing_certificate_loaded()
assert self.existing_certificate is not None
# Check whether certificate is signed by CA certificate
if not cryptography_verify_certificate_signature(
certificate=self.existing_certificate,
signer_public_key=self.ca_cert.public_key(),
):
return True
# Check subject
if self.ca_cert.subject != self.existing_certificate.issuer:
return True
# Check AuthorityKeyIdentifier
if self.create_authority_key_identifier:
try:
ext_ski = self.ca_cert.extensions.get_extension_for_class(
x509.SubjectKeyIdentifier
)
expected_ext = (
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
ext_ski.value
)
)
except cryptography.x509.ExtensionNotFound:
public_key = self.ca_cert.public_key()
assert is_potential_certificate_issuer_public_key(public_key)
expected_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key(
public_key
)
try:
ext_aki = self.existing_certificate.extensions.get_extension_for_class(
x509.AuthorityKeyIdentifier
)
if ext_aki.value != expected_ext:
return True
except cryptography.x509.ExtensionNotFound:
return True
return False
def dump(self, *, include_certificate: bool) -> dict[str, t.Any]:
result = super().dump(include_certificate=include_certificate)
result.update(
{
"ca_cert": self.ca_cert_path,
"ca_privatekey": self.ca_privatekey_path,
}
)
if self.module.check_mode:
result.update(
{
"notBefore": self.notBefore.strftime("%Y%m%d%H%M%SZ"),
"notAfter": self.notAfter.strftime("%Y%m%d%H%M%SZ"),
"serial_number": self.serial_number,
}
)
else:
if self.cert is None:
self.cert = self.existing_certificate
assert self.cert is not None
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"
),
"serial_number": self.cert.serial_number,
}
)
return result
def generate_serial_number() -> int:
"""Generate a serial number for a certificate"""
while True:
result = randrange(0, 1 << 160)
if result >= 1000:
return result
class OwnCACertificateProvider(CertificateProvider):
def validate_module_args(self, module: AnsibleModule) -> None:
if (
module.params["ownca_path"] is None
and module.params["ownca_content"] is None
):
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 create_backend(
self, module: AnsibleModule
) -> OwnCACertificateBackendCryptography:
return OwnCACertificateBackendCryptography(module=module)
def add_ownca_provider_to_argument_spec(argument_spec: ArgumentSpec) -> None:
argument_spec.argument_spec["provider"]["choices"].append("ownca")
argument_spec.argument_spec.update(
{
"ownca_path": {"type": "path"},
"ownca_content": {"type": "str"},
"ownca_privatekey_path": {"type": "path"},
"ownca_privatekey_content": {"type": "str", "no_log": True},
"ownca_privatekey_passphrase": {"type": "str", "no_log": True},
"ownca_digest": {"type": "str", "default": "sha256"},
"ownca_version": {"type": "int", "default": 3, "choices": [3]}, # not used
"ownca_not_before": {"type": "str", "default": "+0s"},
"ownca_not_after": {"type": "str", "default": "+3650d"},
"ownca_create_subject_key_identifier": {
"type": "str",
"default": "create_if_not_provided",
"choices": ["create_if_not_provided", "always_create", "never_create"],
},
"ownca_create_authority_key_identifier": {"type": "bool", "default": True},
}
)
argument_spec.mutually_exclusive.extend(
[
["ownca_path", "ownca_content"],
["ownca_privatekey_path", "ownca_privatekey_content"],
]
)
__all__ = (
"OwnCACertificateBackendCryptography",
"OwnCACertificateProvider",
"add_ownca_provider_to_argument_spec",
)

View File

@@ -0,0 +1,274 @@
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# GNU General Public License v3.0+ (see 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import os
import typing as t
from random import randrange
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_key_needs_digest_for_signing,
cryptography_verify_certificate_signature,
get_not_valid_after,
get_not_valid_before,
is_potential_certificate_issuer_private_key,
set_not_valid_after,
set_not_valid_before,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.certificate import (
CertificateBackend,
CertificateError,
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 (
get_relative_time_option,
)
if t.TYPE_CHECKING:
import datetime
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._argspec import (
ArgumentSpec,
)
from cryptography.hazmat.primitives.asymmetric.types import (
CertificateIssuerPrivateKeyTypes,
)
try:
import cryptography
from cryptography import x509
from cryptography.hazmat.primitives.serialization import Encoding
except ImportError:
pass
class SelfSignedCertificateBackendCryptography(CertificateBackend):
privatekey: CertificateIssuerPrivateKeyTypes
def __init__(self, *, module: AnsibleModule) -> None:
super().__init__(module=module)
self.create_subject_key_identifier: t.Literal[
"create_if_not_provided", "always_create", "never_create"
] = module.params["selfsigned_create_subject_key_identifier"]
self.notBefore = get_relative_time_option(
module.params["selfsigned_not_before"],
input_name="selfsigned_not_before",
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
self.notAfter = get_relative_time_option(
module.params["selfsigned_not_after"],
input_name="selfsigned_not_after",
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
self.digest = select_message_digest(module.params["selfsigned_digest"])
self.serial_number = x509.random_serial_number()
if self.csr_path is not None and not os.path.exists(self.csr_path):
raise CertificateError(
f"The certificate signing request file {self.csr_path} does not exist"
)
if self.privatekey_path is not None and not os.path.exists(
self.privatekey_path
):
raise CertificateError(
f"The private key file {self.privatekey_path} does not exist"
)
self._module = module
self._ensure_private_key_loaded()
if self.privatekey is None:
raise CertificateError("Private key has not been provided")
if not is_potential_certificate_issuer_private_key(self.privatekey):
raise CertificateError("Private key cannot be used to sign certificates")
if cryptography_key_needs_digest_for_signing(self.privatekey):
if self.digest is None:
raise CertificateError(
f"The digest {module.params['selfsigned_digest']} is not supported with the cryptography backend"
)
else:
self.digest = None
self._ensure_csr_loaded()
if self.csr is None:
# Create empty CSR on the fly
csr = cryptography.x509.CertificateSigningRequestBuilder()
csr = csr.subject_name(cryptography.x509.Name([]))
self.csr = csr.sign(self.privatekey, self.digest)
def generate_certificate(self) -> None:
"""(Re-)Generate certificate."""
if self.csr is None:
raise AssertionError("Contract violation: csr has not been populated")
if self.privatekey is None:
raise AssertionError(
"Contract violation: privatekey has not been populated"
)
try:
cert_builder = x509.CertificateBuilder()
cert_builder = cert_builder.subject_name(self.csr.subject)
cert_builder = cert_builder.issuer_name(self.csr.subject)
cert_builder = cert_builder.serial_number(self.serial_number)
cert_builder = set_not_valid_before(cert_builder, self.notBefore)
cert_builder = set_not_valid_after(cert_builder, self.notAfter)
cert_builder = cert_builder.public_key(self.privatekey.public_key())
has_ski = False
for extension in self.csr.extensions:
if isinstance(extension.value, x509.SubjectKeyIdentifier):
if self.create_subject_key_identifier == "always_create":
continue
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(
x509.SubjectKeyIdentifier.from_public_key(
self.privatekey.public_key()
),
critical=False,
)
except ValueError as e:
raise CertificateError(str(e)) from e
certificate = cert_builder.sign(
private_key=self.privatekey,
algorithm=self.digest,
)
self.cert = certificate
def get_certificate_data(self) -> bytes:
"""Return bytes for self.cert."""
if self.cert is None:
raise AssertionError("Contract violation: cert has not been populated")
return self.cert.public_bytes(Encoding.PEM)
def needs_regeneration(
self,
*,
not_before: datetime.datetime | None = None,
not_after: datetime.datetime | None = None,
) -> bool:
assert self.privatekey is not None
if super().needs_regeneration(
not_before=self.notBefore, not_after=self.notAfter
):
return True
self._ensure_existing_certificate_loaded()
assert self.existing_certificate is not None
# Check whether certificate is signed by private key
if not cryptography_verify_certificate_signature(
certificate=self.existing_certificate,
signer_public_key=self.privatekey.public_key(),
):
return True
return False
def dump(self, *, include_certificate: bool) -> dict[str, t.Any]:
result = super().dump(include_certificate=include_certificate)
if self.module.check_mode:
result.update(
{
"notBefore": self.notBefore.strftime("%Y%m%d%H%M%SZ"),
"notAfter": self.notAfter.strftime("%Y%m%d%H%M%SZ"),
"serial_number": self.serial_number,
}
)
else:
if self.cert is None:
self.cert = self.existing_certificate
assert self.cert is not None
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"
),
"serial_number": self.cert.serial_number,
}
)
return result
def generate_serial_number() -> int:
"""Generate a serial number for a certificate"""
while True:
result = randrange(0, 1 << 160)
if result >= 1000:
return result
class SelfSignedCertificateProvider(CertificateProvider):
def validate_module_args(self, module: AnsibleModule) -> None:
if (
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 create_backend(
self, module: AnsibleModule
) -> SelfSignedCertificateBackendCryptography:
return SelfSignedCertificateBackendCryptography(module=module)
def add_selfsigned_provider_to_argument_spec(argument_spec: ArgumentSpec) -> None:
argument_spec.argument_spec["provider"]["choices"].append("selfsigned")
argument_spec.argument_spec.update(
{
"selfsigned_version": {
"type": "int",
"default": 3,
"choices": [3],
}, # not used
"selfsigned_digest": {"type": "str", "default": "sha256"},
"selfsigned_not_before": {
"type": "str",
"default": "+0s",
"aliases": ["selfsigned_notBefore"],
},
"selfsigned_not_after": {
"type": "str",
"default": "+3650d",
"aliases": ["selfsigned_notAfter"],
},
"selfsigned_create_subject_key_identifier": {
"type": "str",
"default": "create_if_not_provided",
"choices": ["create_if_not_provided", "always_create", "never_create"],
},
}
)
__all__ = (
"SelfSignedCertificateBackendCryptography",
"SelfSignedCertificateProvider",
"add_selfsigned_provider_to_argument_spec",
)

View File

@@ -0,0 +1,132 @@
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import typing as t
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_crl import (
TIMESTAMP_FORMAT,
cryptography_decode_revoked_certificate,
cryptography_dump_revoked,
cryptography_get_signature_algorithm_oid_from_crl,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
cryptography_oid_to_name,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.pem import (
identify_pem_format,
)
from ansible_collections.community.crypto.plugins.module_utils._cryptography_dep import (
COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION,
assert_required_cryptography_version,
)
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.plugin_utils._action_module import (
AnsibleActionModule,
)
from ansible_collections.community.crypto.plugins.plugin_utils._filter_module import (
FilterModuleMock,
)
from cryptography.hazmat.primitives.asymmetric.types import (
PrivateKeyTypes,
)
GeneralAnsibleModule = t.Union[AnsibleModule, AnsibleActionModule, FilterModuleMock]
# crypto_utils
MINIMAL_CRYPTOGRAPHY_VERSION = COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION
try:
from cryptography import x509
except ImportError:
pass
class CRLInfoRetrieval:
def __init__(
self,
*,
module: GeneralAnsibleModule,
content: bytes,
list_revoked_certificates: bool = True,
) -> None:
# content must be a bytes string
self.module = module
self.content = content
self.list_revoked_certificates = list_revoked_certificates
self.name_encoding = module.params.get("name_encoding", "ignore")
def get_info(self) -> dict[str, t.Any]:
crl_pem = identify_pem_format(self.content)
try:
if crl_pem:
crl = x509.load_pem_x509_crl(self.content)
else:
crl = x509.load_der_x509_crl(self.content)
except ValueError as e:
self.module.fail_json(msg=f"Error while decoding CRL: {e}")
result: dict[str, t.Any] = {
"changed": False,
"format": "pem" if crl_pem else "der",
"last_update": None,
"next_update": None,
"digest": None,
"issuer_ordered": None,
"issuer": None,
}
result["last_update"] = crl.last_update.strftime(TIMESTAMP_FORMAT)
result["next_update"] = (
crl.next_update.strftime(TIMESTAMP_FORMAT) if crl.next_update else None
)
result["digest"] = cryptography_oid_to_name(
cryptography_get_signature_algorithm_oid_from_crl(crl)
)
issuer = []
for attribute in crl.issuer:
issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value])
result["issuer_ordered"] = issuer
issuer_dict = {}
for k, v in issuer:
issuer_dict[k] = v
result["issuer"] = issuer_dict
if self.list_revoked_certificates:
result["revoked_certificates"] = []
for cert in crl:
entry = cryptography_decode_revoked_certificate(cert)
result["revoked_certificates"].append(
cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding)
)
return result
def get_crl_info(
*,
module: GeneralAnsibleModule,
content: bytes,
list_revoked_certificates: bool = True,
) -> dict[str, t.Any]:
assert_required_cryptography_version(
module, minimum_cryptography_version=MINIMAL_CRYPTOGRAPHY_VERSION
)
info = CRLInfoRetrieval(
module=module,
content=content,
list_revoked_certificates=list_revoked_certificates,
)
return info.get_info()
__all__ = ("CRLInfoRetrieval", "get_crl_info")

View File

@@ -0,0 +1,939 @@
# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import abc
import binascii
import typing as t
from ansible.module_utils.common.text.converters import to_text
from ansible_collections.community.crypto.plugins.module_utils._argspec import (
ArgumentSpec,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
OpenSSLBadPassphraseError,
OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_crl import (
REVOCATION_REASON_MAP,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
cryptography_get_basic_constraints,
cryptography_get_name,
cryptography_key_needs_digest_for_signing,
cryptography_name_to_oid,
cryptography_parse_key_usage_params,
cryptography_parse_relative_distinguished_name,
is_potential_certificate_issuer_public_key,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.csr_info import (
get_csr_info,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.support import (
load_certificate_issuer_privatekey,
load_certificate_request,
parse_name_field,
parse_ordered_name_field,
select_message_digest,
)
from ansible_collections.community.crypto.plugins.module_utils._cryptography_dep import (
COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION,
assert_required_cryptography_version,
)
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
CertificatePrivateKeyTypes,
)
from cryptography.hazmat.primitives.asymmetric.types import (
CertificateIssuerPrivateKeyTypes,
PrivateKeyTypes,
)
_ET = t.TypeVar("_ET", bound="cryptography.x509.ExtensionType")
MINIMAL_CRYPTOGRAPHY_VERSION = COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION
try:
import cryptography
import cryptography.exceptions
import cryptography.hazmat.backends
import cryptography.hazmat.primitives.hashes
import cryptography.hazmat.primitives.serialization
import cryptography.x509
import cryptography.x509.oid
except ImportError:
pass
class CertificateSigningRequestError(OpenSSLObjectError):
pass
# From the object called `module`, only the following properties are used:
#
# - module.params[]
# - module.warn(msg: str)
# - module.fail_json(msg: str, **kwargs)
class CertificateSigningRequestBackend(metaclass=abc.ABCMeta):
def __init__(self, *, module: AnsibleModule) -> None:
self.module = module
self.digest: str = module.params["digest"]
self.privatekey_path: str | None = module.params["privatekey_path"]
privatekey_content: str | None = module.params["privatekey_content"]
if privatekey_content is not None:
self.privatekey_content: bytes | None = privatekey_content.encode("utf-8")
else:
self.privatekey_content = None
self.privatekey_passphrase: str | None = module.params["privatekey_passphrase"]
self.version: t.Literal[1] = module.params["version"]
self.subjectAltName: list[str] | None = module.params["subject_alt_name"]
self.subjectAltName_critical: bool = module.params["subject_alt_name_critical"]
self.keyUsage: list[str] | None = module.params["key_usage"]
self.keyUsage_critical: bool = module.params["key_usage_critical"]
self.extendedKeyUsage: list[str] | None = module.params["extended_key_usage"]
self.extendedKeyUsage_critical: bool = module.params[
"extended_key_usage_critical"
]
self.basicConstraints: list[str] | None = module.params["basic_constraints"]
self.basicConstraints_critical: bool = module.params[
"basic_constraints_critical"
]
self.ocspMustStaple: bool = module.params["ocsp_must_staple"]
self.ocspMustStaple_critical: bool = module.params["ocsp_must_staple_critical"]
self.name_constraints_permitted: list[str] = (
module.params["name_constraints_permitted"] or []
)
self.name_constraints_excluded: list[str] = (
module.params["name_constraints_excluded"] or []
)
self.name_constraints_critical: bool = module.params[
"name_constraints_critical"
]
self.create_subject_key_identifier: bool = module.params[
"create_subject_key_identifier"
]
subject_key_identifier: str | None = module.params["subject_key_identifier"]
authority_key_identifier: str | None = module.params["authority_key_identifier"]
self.authority_cert_issuer: list[str] | None = module.params[
"authority_cert_issuer"
]
self.authority_cert_serial_number: int = module.params[
"authority_cert_serial_number"
]
self.crl_distribution_points: (
list[cryptography.x509.DistributionPoint] | None
) = None
self.csr: cryptography.x509.CertificateSigningRequest | None = None
self.privatekey: CertificateIssuerPrivateKeyTypes | None = None
if self.create_subject_key_identifier and subject_key_identifier is not None:
module.fail_json(
msg="subject_key_identifier cannot be specified if create_subject_key_identifier is true"
)
self.ordered_subject = False
self.subject = [
("C", module.params["country_name"]),
("ST", module.params["state_or_province_name"]),
("L", module.params["locality_name"]),
("O", module.params["organization_name"]),
("OU", module.params["organizational_unit_name"]),
("CN", module.params["common_name"]),
("emailAddress", module.params["email_address"]),
]
self.subject = [(entry[0], entry[1]) for entry in self.subject if entry[1]]
try:
if module.params["subject"]:
self.subject = self.subject + parse_name_field(
module.params["subject"], name_field_name="subject"
)
if module.params["subject_ordered"]:
if self.subject:
raise CertificateSigningRequestError(
"subject_ordered cannot be combined with any other subject field"
)
self.subject = parse_ordered_name_field(
module.params["subject_ordered"], name_field_name="subject_ordered"
)
self.ordered_subject = True
except ValueError as exc:
raise CertificateSigningRequestError(str(exc)) from exc
self.using_common_name_for_san = False
if not self.subjectAltName and module.params["use_common_name_for_san"]:
for sub in self.subject:
if sub[0] in ("commonName", "CN"):
self.subjectAltName = [f"DNS:{sub[1]}"]
self.using_common_name_for_san = True
break
self.subject_key_identifier: bytes | None = None
if subject_key_identifier is not None:
try:
self.subject_key_identifier = binascii.unhexlify(
subject_key_identifier.replace(":", "")
)
except Exception as e:
raise CertificateSigningRequestError(
f"Cannot parse subject_key_identifier: {e}"
) from e
self.authority_key_identifier: bytes | None = None
if authority_key_identifier is not None:
try:
self.authority_key_identifier = binascii.unhexlify(
authority_key_identifier.replace(":", "")
)
except Exception as e:
raise CertificateSigningRequestError(
f"Cannot parse authority_key_identifier: {e}"
) from e
self.existing_csr: cryptography.x509.CertificateSigningRequest | None = None
self.existing_csr_bytes: bytes | None = None
self.diff_before = self._get_info(data=None)
self.diff_after = self._get_info(data=None)
def _get_info(self, *, data: bytes | None) -> dict[str, t.Any]:
if data is None:
return {}
try:
result = get_csr_info(
module=self.module,
content=data,
validate_signature=False,
prefer_one_fingerprint=True,
)
result["can_parse_csr"] = True
return result
except Exception:
return {"can_parse_csr": False}
@abc.abstractmethod
def generate_csr(self) -> None:
"""(Re-)Generate CSR."""
@abc.abstractmethod
def get_csr_data(self) -> bytes:
"""Return bytes for self.csr."""
def set_existing(self, *, csr_bytes: bytes | None) -> None:
"""Set existing CSR bytes. None indicates that the CSR does not exist."""
self.existing_csr_bytes = csr_bytes
self.diff_after = self.diff_before = self._get_info(
data=self.existing_csr_bytes
)
def has_existing(self) -> bool:
"""Query whether an existing CSR is/has been there."""
return self.existing_csr_bytes is not None
def _ensure_private_key_loaded(self) -> None:
"""Load the provided private key into self.privatekey."""
if self.privatekey is not None:
return
try:
self.privatekey = load_certificate_issuer_privatekey(
path=self.privatekey_path,
content=self.privatekey_content,
passphrase=self.privatekey_passphrase,
)
except OpenSSLBadPassphraseError as exc:
raise CertificateSigningRequestError(exc) from exc
@abc.abstractmethod
def _check_csr(self) -> bool:
"""Check whether provided parameters, assuming self.existing_csr and self.privatekey have been populated."""
def needs_regeneration(self) -> bool:
"""Check whether a regeneration is necessary."""
if self.existing_csr_bytes is None:
return True
try:
self.existing_csr = load_certificate_request(
content=self.existing_csr_bytes,
)
except Exception:
return True
self._ensure_private_key_loaded()
return not self._check_csr()
def dump(self, *, include_csr: bool) -> dict[str, t.Any]:
"""Serialize the object into a dictionary."""
result: dict[str, t.Any] = {
"privatekey": self.privatekey_path,
"subject": self.subject,
"subjectAltName": self.subjectAltName,
"keyUsage": self.keyUsage,
"extendedKeyUsage": self.extendedKeyUsage,
"basicConstraints": self.basicConstraints,
"ocspMustStaple": self.ocspMustStaple,
"name_constraints_permitted": self.name_constraints_permitted,
"name_constraints_excluded": self.name_constraints_excluded,
}
# Get hold of CSR bytes
csr_bytes = self.existing_csr_bytes
if self.csr is not None:
csr_bytes = self.get_csr_data()
self.diff_after = self._get_info(data=csr_bytes)
if include_csr:
# Store result
result["csr"] = csr_bytes.decode("utf-8") if csr_bytes else None
result["diff"] = {
"before": self.diff_before,
"after": self.diff_after,
}
return result
def parse_crl_distribution_points(
*, module: AnsibleModule, crl_distribution_points: list[dict[str, t.Any]]
) -> list[cryptography.x509.DistributionPoint]:
result = []
for index, parse_crl_distribution_point in enumerate(crl_distribution_points):
try:
full_name = None
relative_name = None
crl_issuer = None
reasons = None
if parse_crl_distribution_point["full_name"] is not None:
if not parse_crl_distribution_point["full_name"]:
raise OpenSSLObjectError("full_name must not be empty")
full_name = [
cryptography_get_name(name, what="full name")
for name in parse_crl_distribution_point["full_name"]
]
if parse_crl_distribution_point["relative_name"] is not None:
if not parse_crl_distribution_point["relative_name"]:
raise OpenSSLObjectError("relative_name must not be empty")
relative_name = cryptography_parse_relative_distinguished_name(
parse_crl_distribution_point["relative_name"]
)
if parse_crl_distribution_point["crl_issuer"] is not None:
if not parse_crl_distribution_point["crl_issuer"]:
raise OpenSSLObjectError("crl_issuer must not be empty")
crl_issuer = [
cryptography_get_name(name, what="CRL issuer")
for name in parse_crl_distribution_point["crl_issuer"]
]
if parse_crl_distribution_point["reasons"] is not None:
reasons_list = []
for reason in parse_crl_distribution_point["reasons"]:
reasons_list.append(REVOCATION_REASON_MAP[reason])
reasons = frozenset(reasons_list)
result.append(
cryptography.x509.DistributionPoint(
full_name=full_name,
relative_name=relative_name,
crl_issuer=crl_issuer,
reasons=reasons,
)
)
except (OpenSSLObjectError, ValueError) as e:
raise OpenSSLObjectError(
f"Error while parsing CRL distribution point #{index}: {e}"
) from e
return result
# Implementation with using cryptography
class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBackend):
def __init__(self, *, module: AnsibleModule) -> None:
super().__init__(module=module)
if self.version != 1:
module.warn(
"The cryptography backend only supports version 1. (The only valid value according to RFC 2986.)"
)
crl_distribution_points: list[dict[str, t.Any]] | None = module.params[
"crl_distribution_points"
]
if crl_distribution_points:
self.crl_distribution_points = parse_crl_distribution_points(
module=module, crl_distribution_points=crl_distribution_points
)
def generate_csr(self) -> None:
"""(Re-)Generate CSR."""
self._ensure_private_key_loaded()
assert self.privatekey is not None
csr = cryptography.x509.CertificateSigningRequestBuilder()
try:
csr = csr.subject_name(
cryptography.x509.Name(
[
cryptography.x509.NameAttribute(
cryptography_name_to_oid(entry[0]), to_text(entry[1])
)
for entry in self.subject
]
)
)
except ValueError as e:
raise CertificateSigningRequestError(e) from e
if self.subjectAltName:
csr = csr.add_extension(
cryptography.x509.SubjectAlternativeName(
[cryptography_get_name(name) for name in self.subjectAltName]
),
critical=self.subjectAltName_critical,
)
if self.keyUsage:
params = cryptography_parse_key_usage_params(self.keyUsage)
csr = csr.add_extension(
cryptography.x509.KeyUsage(**params), critical=self.keyUsage_critical
)
if self.extendedKeyUsage:
usages = [
cryptography_name_to_oid(usage) for usage in self.extendedKeyUsage
]
csr = csr.add_extension(
cryptography.x509.ExtendedKeyUsage(usages),
critical=self.extendedKeyUsage_critical,
)
if self.basicConstraints:
params = {}
ca, path_length = cryptography_get_basic_constraints(self.basicConstraints)
csr = csr.add_extension(
cryptography.x509.BasicConstraints(ca, path_length),
critical=self.basicConstraints_critical,
)
if self.ocspMustStaple:
csr = csr.add_extension(
cryptography.x509.TLSFeature(
[cryptography.x509.TLSFeatureType.status_request]
),
critical=self.ocspMustStaple_critical,
)
if self.name_constraints_permitted or self.name_constraints_excluded:
try:
csr = csr.add_extension(
cryptography.x509.NameConstraints(
[
cryptography_get_name(
name, what="name constraints permitted"
)
for name in self.name_constraints_permitted
]
or None,
[
cryptography_get_name(
name, what="name constraints excluded"
)
for name in self.name_constraints_excluded
]
or None,
),
critical=self.name_constraints_critical,
)
except TypeError as e:
raise OpenSSLObjectError(
f"Error while parsing name constraint: {e}"
) from e
if self.create_subject_key_identifier:
if not is_potential_certificate_issuer_public_key(
self.privatekey.public_key()
):
raise OpenSSLObjectError(
"Private key can not be used to create subject key identifier"
)
csr = csr.add_extension(
cryptography.x509.SubjectKeyIdentifier.from_public_key(
self.privatekey.public_key()
),
critical=False,
)
elif self.subject_key_identifier is not None:
csr = csr.add_extension(
cryptography.x509.SubjectKeyIdentifier(self.subject_key_identifier),
critical=False,
)
if (
self.authority_key_identifier is not None
or self.authority_cert_issuer is not None
or self.authority_cert_serial_number is not None
):
issuers = None
if self.authority_cert_issuer is not None:
issuers = [
cryptography_get_name(n, what="authority cert issuer")
for n in self.authority_cert_issuer
]
csr = csr.add_extension(
cryptography.x509.AuthorityKeyIdentifier(
self.authority_key_identifier,
issuers,
self.authority_cert_serial_number,
),
critical=False,
)
if self.crl_distribution_points:
csr = csr.add_extension(
cryptography.x509.CRLDistributionPoints(self.crl_distribution_points),
critical=False,
)
# csr.sign() does not accept some digests we theoretically could have in digest.
# For that reason we use type t.Any here. csr.sign() will complain if
# the digest is not acceptable.
digest: t.Any | None = None
if cryptography_key_needs_digest_for_signing(self.privatekey):
digest = select_message_digest(self.digest)
if digest is None:
raise CertificateSigningRequestError(
f'Unsupported digest "{self.digest}"'
)
try:
self.csr = csr.sign(self.privatekey, digest)
except UnicodeError as e:
# This catches IDNAErrors, which happens when a bad name is passed as a SAN
# (https://github.com/ansible-collections/community.crypto/issues/105).
# For older cryptography versions, this is handled by idna, which raises
# an idna.core.IDNAError. Later versions of cryptography deprecated and stopped
# requiring idna, whence we cannot easily handle this error. Fortunately, in
# most versions of idna, IDNAError extends UnicodeError. There is only version
# 2.3 where it extends Exception instead (see
# https://github.com/kjd/idna/commit/ebefacd3134d0f5da4745878620a6a1cba86d130
# and then
# https://github.com/kjd/idna/commit/ea03c7b5db7d2a99af082e0239da2b68aeea702a).
msg = f"Error while creating CSR: {e}\n"
if self.using_common_name_for_san:
self.module.fail_json(
msg=msg
+ "This is probably caused because the Common Name is used as a SAN."
" Specifying use_common_name_for_san=false might fix this."
)
self.module.fail_json(
msg=msg
+ "This is probably caused by an invalid Subject Alternative DNS Name."
)
def get_csr_data(self) -> bytes:
"""Return bytes for self.csr."""
if self.csr is None:
raise AssertionError("Violated contract: csr is not populated")
return self.csr.public_bytes(
cryptography.hazmat.primitives.serialization.Encoding.PEM
)
def _check_csr(self) -> bool:
"""Check whether provided parameters, assuming self.existing_csr and self.privatekey have been populated."""
if self.existing_csr is None:
raise AssertionError("Violated contract: existing_csr is not populated")
if self.privatekey is None:
raise AssertionError("Violated contract: privatekey is not populated")
def _check_subject(csr: cryptography.x509.CertificateSigningRequest) -> bool:
subject = [
(cryptography_name_to_oid(entry[0]), to_text(entry[1]))
for entry in self.subject
]
current_subject = [(sub.oid, sub.value) for sub in csr.subject]
if self.ordered_subject:
return subject == current_subject
return set(subject) == set(current_subject)
def _find_extension(
extensions: cryptography.x509.Extensions, exttype: type[_ET]
) -> cryptography.x509.Extension[_ET] | None:
return next(
(ext for ext in extensions if isinstance(ext.value, exttype)), None
)
def _check_subjectAltName(extensions: cryptography.x509.Extensions) -> bool:
current_altnames_ext = _find_extension(
extensions, cryptography.x509.SubjectAlternativeName
)
current_altnames = (
[to_text(altname) for altname in current_altnames_ext.value]
if current_altnames_ext
else []
)
altnames = (
[
to_text(cryptography_get_name(altname))
for altname in self.subjectAltName
]
if self.subjectAltName
else []
)
if set(altnames) != set(current_altnames):
return False
if altnames and current_altnames_ext:
if current_altnames_ext.critical != self.subjectAltName_critical:
return False
return True
def _check_keyUsage(extensions: cryptography.x509.Extensions) -> bool:
current_keyusage_ext = _find_extension(
extensions, cryptography.x509.KeyUsage
)
if not self.keyUsage:
return current_keyusage_ext is None
if current_keyusage_ext is None:
return False
params = cryptography_parse_key_usage_params(self.keyUsage)
for param, value in params.items():
# TODO: check whether getattr() with '_' prepended is really needed
if getattr(current_keyusage_ext.value, "_" + param) != value:
return False
return current_keyusage_ext.critical == self.keyUsage_critical
def _check_extenededKeyUsage(extensions: cryptography.x509.Extensions) -> bool:
current_usages_ext = _find_extension(
extensions, cryptography.x509.ExtendedKeyUsage
)
current_usages = (
[str(usage) for usage in current_usages_ext.value]
if current_usages_ext
else []
)
usages = (
[
str(cryptography_name_to_oid(usage))
for usage in self.extendedKeyUsage
]
if self.extendedKeyUsage
else []
)
if set(current_usages) != set(usages):
return False
if usages and current_usages_ext:
if current_usages_ext.critical != self.extendedKeyUsage_critical:
return False
return True
def _check_basicConstraints(extensions: cryptography.x509.Extensions) -> bool:
bc_ext = _find_extension(extensions, cryptography.x509.BasicConstraints)
current_ca = bc_ext.value.ca if bc_ext else False
current_path_length = bc_ext.value.path_length if bc_ext else None
ca, path_length = cryptography_get_basic_constraints(self.basicConstraints)
# Check CA flag
if ca != current_ca:
return False
# Check path length
if path_length != current_path_length:
return False
# Check criticality
if self.basicConstraints:
return (
bc_ext is not None
and bc_ext.critical == self.basicConstraints_critical
)
return bc_ext is None
def _check_ocspMustStaple(extensions: cryptography.x509.Extensions) -> bool:
tlsfeature_ext = _find_extension(extensions, cryptography.x509.TLSFeature)
if self.ocspMustStaple:
if (
not tlsfeature_ext
or tlsfeature_ext.critical != self.ocspMustStaple_critical
):
return False
return (
cryptography.x509.TLSFeatureType.status_request
in tlsfeature_ext.value
)
return tlsfeature_ext is None
def _check_nameConstraints(extensions: cryptography.x509.Extensions) -> bool:
current_nc_ext = _find_extension(
extensions, cryptography.x509.NameConstraints
)
current_nc_perm = (
[
to_text(altname)
for altname in current_nc_ext.value.permitted_subtrees or []
]
if current_nc_ext
else []
)
current_nc_excl = (
[
to_text(altname)
for altname in current_nc_ext.value.excluded_subtrees or []
]
if current_nc_ext
else []
)
nc_perm = [
to_text(
cryptography_get_name(altname, what="name constraints permitted")
)
for altname in self.name_constraints_permitted
]
nc_excl = [
to_text(
cryptography_get_name(altname, what="name constraints excluded")
)
for altname in self.name_constraints_excluded
]
if set(nc_perm) != set(current_nc_perm) or set(nc_excl) != set(
current_nc_excl
):
return False
if (nc_perm or nc_excl) and current_nc_ext:
if current_nc_ext.critical != self.name_constraints_critical:
return False
return True
def _check_subject_key_identifier(
extensions: cryptography.x509.Extensions,
) -> bool:
ext = _find_extension(extensions, cryptography.x509.SubjectKeyIdentifier)
if (
self.create_subject_key_identifier
or self.subject_key_identifier is not None
):
if not ext or ext.critical:
return False
if self.create_subject_key_identifier:
assert self.privatekey is not None
digest = cryptography.x509.SubjectKeyIdentifier.from_public_key(
self.privatekey.public_key()
).digest
return ext.value.digest == digest
return ext.value.digest == self.subject_key_identifier
return ext is None
def _check_authority_key_identifier(
extensions: cryptography.x509.Extensions,
) -> bool:
ext = _find_extension(extensions, cryptography.x509.AuthorityKeyIdentifier)
if (
self.authority_key_identifier is not None
or self.authority_cert_issuer is not None
or self.authority_cert_serial_number is not None
):
if not ext or ext.critical:
return False
aci = None
csr_aci = None
if self.authority_cert_issuer is not None:
aci = [
to_text(cryptography_get_name(n, what="authority cert issuer"))
for n in self.authority_cert_issuer
]
if ext.value.authority_cert_issuer is not None:
csr_aci = [to_text(n) for n in ext.value.authority_cert_issuer]
return (
ext.value.key_identifier == self.authority_key_identifier
and csr_aci == aci
and ext.value.authority_cert_serial_number
== self.authority_cert_serial_number
)
return ext is None
def _check_crl_distribution_points(
extensions: cryptography.x509.Extensions,
) -> bool:
ext = _find_extension(extensions, cryptography.x509.CRLDistributionPoints)
if self.crl_distribution_points is None:
return ext is None
if not ext:
return False
return list(ext.value) == self.crl_distribution_points
def _check_extensions(csr: cryptography.x509.CertificateSigningRequest) -> bool:
extensions = csr.extensions
return (
_check_subjectAltName(extensions)
and _check_keyUsage(extensions)
and _check_extenededKeyUsage(extensions)
and _check_basicConstraints(extensions)
and _check_ocspMustStaple(extensions)
and _check_subject_key_identifier(extensions)
and _check_authority_key_identifier(extensions)
and _check_nameConstraints(extensions)
and _check_crl_distribution_points(extensions)
)
def _check_signature(csr: cryptography.x509.CertificateSigningRequest) -> bool:
if not csr.is_signature_valid:
return False
# To check whether public key of CSR belongs to private key,
# encode both public keys and compare PEMs.
key_a = csr.public_key().public_bytes(
cryptography.hazmat.primitives.serialization.Encoding.PEM,
cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo,
)
assert self.privatekey is not None
key_b = self.privatekey.public_key().public_bytes(
cryptography.hazmat.primitives.serialization.Encoding.PEM,
cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo,
)
return key_a == key_b
return (
_check_subject(self.existing_csr)
and _check_extensions(self.existing_csr)
and _check_signature(self.existing_csr)
)
def select_backend(
module: AnsibleModule,
) -> CertificateSigningRequestCryptographyBackend:
assert_required_cryptography_version(
module, minimum_cryptography_version=MINIMAL_CRYPTOGRAPHY_VERSION
)
return CertificateSigningRequestCryptographyBackend(module=module)
def get_csr_argument_spec() -> ArgumentSpec:
return ArgumentSpec(
argument_spec={
"digest": {"type": "str", "default": "sha256"},
"privatekey_path": {"type": "path"},
"privatekey_content": {"type": "str", "no_log": True},
"privatekey_passphrase": {"type": "str", "no_log": True},
"version": {"type": "int", "default": 1, "choices": [1]},
"subject": {"type": "dict"},
"subject_ordered": {"type": "list", "elements": "dict"},
"country_name": {"type": "str", "aliases": ["C", "countryName"]},
"state_or_province_name": {
"type": "str",
"aliases": ["ST", "stateOrProvinceName"],
},
"locality_name": {"type": "str", "aliases": ["L", "localityName"]},
"organization_name": {"type": "str", "aliases": ["O", "organizationName"]},
"organizational_unit_name": {
"type": "str",
"aliases": ["OU", "organizationalUnitName"],
},
"common_name": {"type": "str", "aliases": ["CN", "commonName"]},
"email_address": {"type": "str", "aliases": ["E", "emailAddress"]},
"subject_alt_name": {
"type": "list",
"elements": "str",
"aliases": ["subjectAltName"],
},
"subject_alt_name_critical": {
"type": "bool",
"default": False,
"aliases": ["subjectAltName_critical"],
},
"use_common_name_for_san": {
"type": "bool",
"default": True,
"aliases": ["useCommonNameForSAN"],
},
"key_usage": {"type": "list", "elements": "str", "aliases": ["keyUsage"]},
"key_usage_critical": {
"type": "bool",
"default": False,
"aliases": ["keyUsage_critical"],
},
"extended_key_usage": {
"type": "list",
"elements": "str",
"aliases": ["extKeyUsage", "extendedKeyUsage"],
},
"extended_key_usage_critical": {
"type": "bool",
"default": False,
"aliases": ["extKeyUsage_critical", "extendedKeyUsage_critical"],
},
"basic_constraints": {
"type": "list",
"elements": "str",
"aliases": ["basicConstraints"],
},
"basic_constraints_critical": {
"type": "bool",
"default": False,
"aliases": ["basicConstraints_critical"],
},
"ocsp_must_staple": {
"type": "bool",
"default": False,
"aliases": ["ocspMustStaple"],
},
"ocsp_must_staple_critical": {
"type": "bool",
"default": False,
"aliases": ["ocspMustStaple_critical"],
},
"name_constraints_permitted": {"type": "list", "elements": "str"},
"name_constraints_excluded": {"type": "list", "elements": "str"},
"name_constraints_critical": {"type": "bool", "default": False},
"create_subject_key_identifier": {"type": "bool", "default": False},
"subject_key_identifier": {"type": "str"},
"authority_key_identifier": {"type": "str"},
"authority_cert_issuer": {"type": "list", "elements": "str"},
"authority_cert_serial_number": {"type": "int"},
"crl_distribution_points": {
"type": "list",
"elements": "dict",
"options": {
"full_name": {"type": "list", "elements": "str"},
"relative_name": {"type": "list", "elements": "str"},
"crl_issuer": {"type": "list", "elements": "str"},
"reasons": {
"type": "list",
"elements": "str",
"choices": [
"key_compromise",
"ca_compromise",
"affiliation_changed",
"superseded",
"cessation_of_operation",
"certificate_hold",
"privilege_withdrawn",
"aa_compromise",
],
},
},
"mutually_exclusive": [("full_name", "relative_name")],
"required_one_of": [("full_name", "relative_name", "crl_issuer")],
},
"select_crypto_backend": {
"type": "str",
"default": "auto",
"choices": ["auto", "cryptography"],
},
},
required_together=[
["authority_cert_issuer", "authority_cert_serial_number"],
],
mutually_exclusive=[
["privatekey_path", "privatekey_content"],
["subject", "subject_ordered"],
],
required_one_of=[
["privatekey_path", "privatekey_content"],
],
)
__all__ = (
"CertificateSigningRequestError",
"CertificateSigningRequestBackend",
"select_backend",
"get_csr_argument_spec",
)

View File

@@ -0,0 +1,399 @@
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import abc
import binascii
import typing as t
from ansible.module_utils.common.text.converters import to_text
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
cryptography_decode_name,
cryptography_get_extensions_from_csr,
cryptography_oid_to_name,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.publickey_info import (
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._cryptography_dep import (
COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION,
assert_required_cryptography_version,
)
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.plugin_utils._action_module import (
AnsibleActionModule,
)
from ansible_collections.community.crypto.plugins.plugin_utils._filter_module import (
FilterModuleMock,
)
from cryptography.hazmat.primitives.asymmetric.types import (
CertificatePublicKeyTypes,
PrivateKeyTypes,
)
GeneralAnsibleModule = t.Union[AnsibleModule, AnsibleActionModule, FilterModuleMock]
MINIMAL_CRYPTOGRAPHY_VERSION = COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION
try:
import cryptography
from cryptography import x509
from cryptography.hazmat.primitives import serialization
except ImportError:
pass
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
class CSRInfoRetrieval(metaclass=abc.ABCMeta):
csr: x509.CertificateSigningRequest
def __init__(
self, *, module: GeneralAnsibleModule, content: bytes, validate_signature: bool
) -> None:
self.module = module
self.content = content
self.validate_signature = validate_signature
@abc.abstractmethod
def _get_subject_ordered(self) -> list[list[str]]:
pass
@abc.abstractmethod
def _get_key_usage(self) -> tuple[list[str] | None, bool]:
pass
@abc.abstractmethod
def _get_extended_key_usage(self) -> tuple[list[str] | None, bool]:
pass
@abc.abstractmethod
def _get_basic_constraints(self) -> tuple[list[str] | None, bool]:
pass
@abc.abstractmethod
def _get_ocsp_must_staple(self) -> tuple[bool | None, bool]:
pass
@abc.abstractmethod
def _get_subject_alt_name(self) -> tuple[list[str] | None, bool]:
pass
@abc.abstractmethod
def _get_name_constraints(self) -> tuple[list[str] | None, list[str] | None, bool]:
pass
@abc.abstractmethod
def _get_public_key_pem(self) -> bytes:
pass
@abc.abstractmethod
def _get_public_key_object(self) -> CertificatePublicKeyTypes:
pass
@abc.abstractmethod
def _get_subject_key_identifier(self) -> bytes | None:
pass
@abc.abstractmethod
def _get_authority_key_identifier(
self,
) -> tuple[bytes | None, list[str] | None, int | None]:
pass
@abc.abstractmethod
def _get_all_extensions(self) -> dict[str, dict[str, bool | str]]:
pass
@abc.abstractmethod
def _is_signature_valid(self) -> bool:
pass
def get_info(self, *, prefer_one_fingerprint: bool = False) -> dict[str, t.Any]:
result: dict[str, t.Any] = {}
self.csr = load_certificate_request(
content=self.content,
)
subject = self._get_subject_ordered()
result["subject"] = {}
for k, v in subject:
result["subject"][k] = v
result["subject_ordered"] = subject
result["key_usage"], result["key_usage_critical"] = self._get_key_usage()
result["extended_key_usage"], result["extended_key_usage_critical"] = (
self._get_extended_key_usage()
)
result["basic_constraints"], result["basic_constraints_critical"] = (
self._get_basic_constraints()
)
result["ocsp_must_staple"], result["ocsp_must_staple_critical"] = (
self._get_ocsp_must_staple()
)
result["subject_alt_name"], result["subject_alt_name_critical"] = (
self._get_subject_alt_name()
)
(
result["name_constraints_permitted"],
result["name_constraints_excluded"],
result["name_constraints_critical"],
) = self._get_name_constraints()
result["public_key"] = to_text(self._get_public_key_pem())
public_key_info = get_publickey_info(
module=self.module,
key=self._get_public_key_object(),
prefer_one_fingerprint=prefer_one_fingerprint,
)
result.update(
{
"public_key_type": public_key_info["type"],
"public_key_data": public_key_info["public_data"],
"public_key_fingerprints": public_key_info["fingerprints"],
}
)
ski_bytes = self._get_subject_key_identifier()
ski = None
if ski_bytes is not None:
ski = binascii.hexlify(ski_bytes).decode("ascii")
ski = ":".join([ski[i : i + 2] for i in range(0, len(ski), 2)])
result["subject_key_identifier"] = ski
aki_bytes, aci, acsn = self._get_authority_key_identifier()
aki = None
if aki_bytes is not None:
aki = binascii.hexlify(aki_bytes).decode("ascii")
aki = ":".join([aki[i : i + 2] for i in range(0, len(aki), 2)])
result["authority_key_identifier"] = aki
result["authority_cert_issuer"] = aci
result["authority_cert_serial_number"] = acsn
result["extensions_by_oid"] = self._get_all_extensions()
result["signature_valid"] = self._is_signature_valid()
if self.validate_signature and not result["signature_valid"]:
self.module.fail_json(msg="CSR signature is invalid!", **result)
return result
class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
"""Validate the supplied CSR, using the cryptography backend"""
def __init__(
self, *, module: GeneralAnsibleModule, content: bytes, validate_signature: bool
) -> None:
super().__init__(
module=module, content=content, validate_signature=validate_signature
)
self.name_encoding: t.Literal["ignore", "idna", "unicode"] = module.params.get(
"name_encoding", "ignore"
)
def _get_subject_ordered(self) -> list[list[str]]:
result: list[list[str]] = []
for attribute in self.csr.subject:
result.append(
[cryptography_oid_to_name(attribute.oid), to_text(attribute.value)]
)
return result
def _get_key_usage(self) -> tuple[list[str] | None, bool]:
try:
current_key_ext = self.csr.extensions.get_extension_for_class(x509.KeyUsage)
current_key_usage = current_key_ext.value
key_usage = {
"digital_signature": current_key_usage.digital_signature,
"content_commitment": current_key_usage.content_commitment,
"key_encipherment": current_key_usage.key_encipherment,
"data_encipherment": current_key_usage.data_encipherment,
"key_agreement": current_key_usage.key_agreement,
"key_cert_sign": current_key_usage.key_cert_sign,
"crl_sign": current_key_usage.crl_sign,
"encipher_only": False,
"decipher_only": False,
}
if key_usage["key_agreement"]:
key_usage.update(
{
"encipher_only": current_key_usage.encipher_only,
"decipher_only": current_key_usage.decipher_only,
}
)
key_usage_names = {
"digital_signature": "Digital Signature",
"content_commitment": "Non Repudiation",
"key_encipherment": "Key Encipherment",
"data_encipherment": "Data Encipherment",
"key_agreement": "Key Agreement",
"key_cert_sign": "Certificate Sign",
"crl_sign": "CRL Sign",
"encipher_only": "Encipher Only",
"decipher_only": "Decipher Only",
}
return (
sorted(
[
key_usage_names[name]
for name, value in key_usage.items()
if value
]
),
current_key_ext.critical,
)
except cryptography.x509.ExtensionNotFound:
return None, False
def _get_extended_key_usage(self) -> tuple[list[str] | None, bool]:
try:
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(
x509.ExtendedKeyUsage
)
return (
sorted(
[cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value]
),
ext_keyusage_ext.critical,
)
except cryptography.x509.ExtensionNotFound:
return None, False
def _get_basic_constraints(self) -> tuple[list[str] | None, bool]:
try:
ext_keyusage_ext = self.csr.extensions.get_extension_for_class(
x509.BasicConstraints
)
result = [f"CA:{'TRUE' if ext_keyusage_ext.value.ca else 'FALSE'}"]
if ext_keyusage_ext.value.path_length is not None:
result.append(f"pathlen:{ext_keyusage_ext.value.path_length}")
return sorted(result), ext_keyusage_ext.critical
except cryptography.x509.ExtensionNotFound:
return None, False
def _get_ocsp_must_staple(self) -> tuple[bool | None, bool]:
try:
# This only works with cryptography >= 2.1
tlsfeature_ext = self.csr.extensions.get_extension_for_class(
x509.TLSFeature
)
value = (
cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
)
return value, tlsfeature_ext.critical
except cryptography.x509.ExtensionNotFound:
return None, False
def _get_subject_alt_name(self) -> tuple[list[str] | None, bool]:
try:
san_ext = self.csr.extensions.get_extension_for_class(
x509.SubjectAlternativeName
)
result = [
cryptography_decode_name(san, idn_rewrite=self.name_encoding)
for san in san_ext.value
]
return result, san_ext.critical
except cryptography.x509.ExtensionNotFound:
return None, False
def _get_name_constraints(self) -> tuple[list[str] | None, list[str] | None, bool]:
try:
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 []
]
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
except cryptography.x509.ExtensionNotFound:
return None, None, False
def _get_public_key_pem(self) -> bytes:
return self.csr.public_key().public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo,
)
def _get_public_key_object(self) -> CertificatePublicKeyTypes:
return self.csr.public_key()
def _get_subject_key_identifier(self) -> bytes | None:
try:
ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
return ext.value.digest
except cryptography.x509.ExtensionNotFound:
return None
def _get_authority_key_identifier(
self,
) -> tuple[bytes | None, list[str] | None, int | None]:
try:
ext = self.csr.extensions.get_extension_for_class(
x509.AuthorityKeyIdentifier
)
issuer = None
if ext.value.authority_cert_issuer is not None:
issuer = [
cryptography_decode_name(san, 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:
return None, None, None
def _get_all_extensions(self) -> dict[str, dict[str, bool | str]]:
return cryptography_get_extensions_from_csr(self.csr)
def _is_signature_valid(self) -> bool:
return self.csr.is_signature_valid
def get_csr_info(
*,
module: GeneralAnsibleModule,
content: bytes,
validate_signature: bool = True,
prefer_one_fingerprint: bool = False,
) -> dict[str, t.Any]:
info = CSRInfoRetrievalCryptography(
module=module, content=content, validate_signature=validate_signature
)
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
def select_backend(
*, module: GeneralAnsibleModule, content: bytes, validate_signature: bool = True
) -> CSRInfoRetrieval:
assert_required_cryptography_version(
module, minimum_cryptography_version=MINIMAL_CRYPTOGRAPHY_VERSION
)
return CSRInfoRetrievalCryptography(
module=module, content=content, validate_signature=validate_signature
)
__all__ = ("CSRInfoRetrieval", "get_csr_info", "select_backend")

View File

@@ -0,0 +1,676 @@
# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import abc
import base64
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils._argspec import (
ArgumentSpec,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.privatekey_info import (
PrivateKeyConsistencyError,
PrivateKeyParseError,
get_privatekey_info,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.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._cryptography_dep import (
COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION,
assert_required_cryptography_version,
)
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.plugin_utils._action_module import (
AnsibleActionModule,
)
from cryptography.hazmat.primitives.asymmetric.types import (
PrivateKeyTypes,
)
GeneralAnsibleModule = t.Union[AnsibleModule, AnsibleActionModule]
MINIMAL_CRYPTOGRAPHY_VERSION = COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION
try:
import cryptography
import cryptography.exceptions
import cryptography.hazmat.backends
import cryptography.hazmat.primitives.asymmetric.dsa
import cryptography.hazmat.primitives.asymmetric.ec
import cryptography.hazmat.primitives.asymmetric.ed448
import cryptography.hazmat.primitives.asymmetric.ed25519
import cryptography.hazmat.primitives.asymmetric.rsa
import cryptography.hazmat.primitives.asymmetric.utils
import cryptography.hazmat.primitives.asymmetric.x448
import cryptography.hazmat.primitives.asymmetric.x25519
import cryptography.hazmat.primitives.serialization
except ImportError:
pass
class PrivateKeyError(OpenSSLObjectError):
pass
# From the object called `module`, only the following properties are used:
#
# - module.params[]
# - module.warn(msg: str)
# - module.fail_json(msg: str, **kwargs)
class PrivateKeyBackend(metaclass=abc.ABCMeta):
def __init__(self, *, module: GeneralAnsibleModule) -> None:
self.module = module
self.type: t.Literal[
"DSA", "ECC", "Ed25519", "Ed448", "RSA", "X25519", "X448"
] = module.params["type"]
self.size: int = module.params["size"]
self.curve: str | None = module.params["curve"]
self.passphrase: str | None = module.params["passphrase"]
self.cipher: str = module.params["cipher"]
self.format: t.Literal["pkcs1", "pkcs8", "raw", "auto", "auto_ignore"] = (
module.params["format"]
)
self.format_mismatch: t.Literal["regenerate", "convert"] = module.params.get(
"format_mismatch", "regenerate"
)
self.regenerate: t.Literal[
"never", "fail", "partial_idempotence", "full_idempotence", "always"
] = module.params.get("regenerate", "full_idempotence")
self.private_key: PrivateKeyTypes | None = None
self.existing_private_key: PrivateKeyTypes | None = None
self.existing_private_key_bytes: bytes | None = None
self.diff_before = self._get_info(data=None)
self.diff_after = self._get_info(data=None)
def _get_info(self, *, data: bytes | None) -> dict[str, t.Any]:
if data is None:
return {}
result: dict[str, t.Any] = {"can_parse_key": False}
try:
result.update(
get_privatekey_info(
module=self.module,
content=data,
passphrase=self.passphrase,
return_private_key_data=False,
prefer_one_fingerprint=True,
)
)
except PrivateKeyConsistencyError as exc:
result.update(exc.result)
except PrivateKeyParseError as exc:
result.update(exc.result)
except Exception:
pass
return result
@abc.abstractmethod
def generate_private_key(self) -> None:
"""(Re-)Generate private key."""
def convert_private_key(self) -> None:
"""Convert existing private key (self.existing_private_key) to new private key (self.private_key).
This is effectively a copy without active conversion. The conversion is done
during load and store; get_private_key_data() uses the destination format to
serialize the key.
"""
self._ensure_existing_private_key_loaded()
self.private_key = self.existing_private_key
@abc.abstractmethod
def get_private_key_data(self) -> bytes:
"""Return bytes for self.private_key."""
def set_existing(self, *, privatekey_bytes: bytes | None) -> None:
"""Set existing private key bytes. None indicates that the key does not exist."""
self.existing_private_key_bytes = privatekey_bytes
self.diff_after = self.diff_before = self._get_info(
data=self.existing_private_key_bytes
)
def has_existing(self) -> bool:
"""Query whether an existing private key is/has been there."""
return self.existing_private_key_bytes is not None
@abc.abstractmethod
def _check_passphrase(self) -> bool:
"""Check whether provided passphrase matches, assuming self.existing_private_key_bytes has been populated."""
@abc.abstractmethod
def _ensure_existing_private_key_loaded(self) -> None:
"""Make sure that self.existing_private_key is populated from self.existing_private_key_bytes."""
@abc.abstractmethod
def _check_size_and_type(self) -> bool:
"""Check whether provided size and type matches, assuming self.existing_private_key has been populated."""
@abc.abstractmethod
def _check_format(self) -> bool:
"""Check whether the key file format, assuming self.existing_private_key and self.existing_private_key_bytes has been populated."""
def needs_regeneration(self) -> bool:
"""Check whether a regeneration is necessary."""
if self.regenerate == "always":
return True
if not self.has_existing():
# key does not exist
return True
if not self._check_passphrase():
if self.regenerate == "full_idempotence":
return True
self.module.fail_json(
msg="Unable to read the key. The key is protected with a another passphrase / no passphrase or broken."
" 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()
if self.regenerate != "never":
if not self._check_size_and_type():
if self.regenerate in ("partial_idempotence", "full_idempotence"):
return True
self.module.fail_json(
msg="Key has wrong type and/or size."
" Will not proceed. To force regeneration, call the module with `generate`"
" set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=true`."
)
# During generation step, regenerate if format does not match and format_mismatch == 'regenerate'
if self.format_mismatch == "regenerate" and self.regenerate != "never":
if not self._check_format():
if self.regenerate in ("partial_idempotence", "full_idempotence"):
return True
self.module.fail_json(
msg="Key has wrong format."
" Will not proceed. To force regeneration, call the module with `generate`"
" set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=true`."
" To convert the key, set `format_mismatch` to `convert`."
)
return False
def needs_conversion(self) -> bool:
"""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'
self._ensure_existing_private_key_loaded()
return (
self.has_existing()
and self.format_mismatch == "convert"
and not self._check_format()
)
def _get_fingerprint(self) -> dict[str, str] | None:
if self.private_key:
return get_fingerprint_of_privatekey(self.private_key)
try:
self._ensure_existing_private_key_loaded()
except Exception:
# Ignore errors
pass
if self.existing_private_key:
return get_fingerprint_of_privatekey(self.existing_private_key)
return None
def dump(self, *, include_key: bool) -> dict[str, t.Any]:
"""Serialize the object into a dictionary."""
if not self.private_key:
try:
self._ensure_existing_private_key_loaded()
except Exception:
# Ignore errors
pass
result: dict[str, t.Any] = {
"type": self.type,
"size": self.size,
"fingerprint": self._get_fingerprint(),
}
if self.type == "ECC":
result["curve"] = self.curve
# Get hold of private key bytes
pk_bytes = self.existing_private_key_bytes
if self.private_key is not None:
pk_bytes = self.get_private_key_data()
self.diff_after = self._get_info(data=pk_bytes)
if include_key:
# Store result
if pk_bytes:
if identify_private_key_format(pk_bytes) == "raw":
result["privatekey"] = base64.b64encode(pk_bytes)
else:
result["privatekey"] = pk_bytes.decode("utf-8")
else:
result["privatekey"] = None
result["diff"] = {
"before": self.diff_before,
"after": self.diff_after,
}
return result
class _Curve:
def __init__(
self,
*,
name: str,
ectype: str,
deprecated: bool,
) -> None:
self.name = name
self.ectype = ectype
self.deprecated = deprecated
def _get_ec_class(
self, *, module: GeneralAnsibleModule
) -> type[cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve]:
ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(self.ectype) # type: ignore
if ecclass is None:
module.fail_json(
msg=f"Your cryptography version does not support {self.ectype}"
)
return ecclass
def create(
self, *, size: int, module: GeneralAnsibleModule
) -> cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve:
ecclass = self._get_ec_class(module=module)
return ecclass()
def verify(
self,
*,
privatekey: cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey,
module: GeneralAnsibleModule,
) -> bool:
ecclass = self._get_ec_class(module=module)
return isinstance(privatekey.private_numbers().public_numbers.curve, ecclass)
# Implementation with using cryptography
class PrivateKeyCryptographyBackend(PrivateKeyBackend):
def _add_curve(
self,
name: str,
ectype: str,
*,
deprecated: bool = False,
) -> None:
self.curves[name] = _Curve(name=name, ectype=ectype, deprecated=deprecated)
def __init__(self, module: GeneralAnsibleModule) -> None:
super().__init__(module=module)
self.curves: dict[str, _Curve] = {}
self._add_curve("secp224r1", "SECP224R1")
self._add_curve("secp256k1", "SECP256K1")
self._add_curve("secp256r1", "SECP256R1")
self._add_curve("secp384r1", "SECP384R1")
self._add_curve("secp521r1", "SECP521R1")
self._add_curve("secp192r1", "SECP192R1", deprecated=True)
self._add_curve("sect163k1", "SECT163K1", deprecated=True)
self._add_curve("sect163r2", "SECT163R2", deprecated=True)
self._add_curve("sect233k1", "SECT233K1", deprecated=True)
self._add_curve("sect233r1", "SECT233R1", deprecated=True)
self._add_curve("sect283k1", "SECT283K1", deprecated=True)
self._add_curve("sect283r1", "SECT283R1", deprecated=True)
self._add_curve("sect409k1", "SECT409K1", deprecated=True)
self._add_curve("sect409r1", "SECT409R1", deprecated=True)
self._add_curve("sect571k1", "SECT571K1", deprecated=True)
self._add_curve("sect571r1", "SECT571R1", deprecated=True)
self._add_curve("brainpoolP256r1", "BrainpoolP256R1", deprecated=True)
self._add_curve("brainpoolP384r1", "BrainpoolP384R1", deprecated=True)
self._add_curve("brainpoolP512r1", "BrainpoolP512R1", deprecated=True)
def _get_wanted_format(self) -> t.Literal["pkcs1", "pkcs8", "raw"]:
if self.format not in ("auto", "auto_ignore"):
return self.format # type: ignore
if self.type in ("X25519", "X448", "Ed25519", "Ed448"):
return "pkcs8"
return "pkcs1"
def generate_private_key(self) -> None:
"""(Re-)Generate private key."""
try:
if self.type == "RSA":
self.private_key = (
cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key(
public_exponent=65537, # OpenSSL always uses this
key_size=self.size,
)
)
if self.type == "DSA":
self.private_key = (
cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key(
key_size=self.size
)
)
if self.type == "X25519":
self.private_key = (
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate()
)
if self.type == "X448":
self.private_key = (
cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate()
)
if self.type == "Ed25519":
self.private_key = (
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate()
)
if 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(
f"Elliptic curves of type {self.curve} should not be used for new keys!"
)
self.private_key = (
cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(
curve=self.curves[self.curve].create(
size=self.size, module=self.module
),
)
)
except cryptography.exceptions.UnsupportedAlgorithm:
self.module.fail_json(
msg=f"Cryptography backend does not support the algorithm required for {self.type}"
)
def get_private_key_data(self) -> bytes:
"""Return bytes for self.private_key"""
if self.private_key is None:
raise AssertionError("private_key not set")
# Select export format and encoding
try:
export_format_txt = self._get_wanted_format()
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM
if export_format_txt == "pkcs1":
# "TraditionalOpenSSL" format is PKCS1
export_format = (
cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL
)
elif export_format_txt == "pkcs8":
export_format = (
cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
)
elif export_format_txt == "raw":
export_format = (
cryptography.hazmat.primitives.serialization.PrivateFormat.Raw
)
export_encoding = (
cryptography.hazmat.primitives.serialization.Encoding.Raw
)
else:
# pylint does not notice that all possible values for export_format_txt have been covered.
raise AssertionError("Can never be reached") # pragma: no cover
except AttributeError:
self.module.fail_json(
msg=f'Cryptography backend does not support the selected output format "{self.format}"'
)
# Select key encryption
encryption_algorithm: (
cryptography.hazmat.primitives.serialization.KeySerializationEncryption
) = cryptography.hazmat.primitives.serialization.NoEncryption()
if self.cipher and self.passphrase:
if self.cipher == "auto":
encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(
to_bytes(self.passphrase)
)
else:
self.module.fail_json(
msg='Cryptography backend can only use "auto" for cipher option.'
)
# Serialize key
try:
return self.private_key.private_bytes(
encoding=export_encoding,
format=export_format,
encryption_algorithm=encryption_algorithm,
)
except ValueError:
self.module.fail_json(
msg=f'Cryptography backend cannot serialize the private key in the required format "{self.format}"'
)
except Exception:
self.module.fail_json(
msg=f'Error while serializing the private key in the required format "{self.format}"',
exception=traceback.format_exc(),
)
def _load_privatekey(self) -> PrivateKeyTypes:
data = self.existing_private_key_bytes
if data is None:
raise AssertionError("existing_private_key_bytes not set")
try:
# Interpret bytes depending on format.
key_format = identify_private_key_format(data)
if key_format == "raw":
if len(data) == 56:
return cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(
data
)
if len(data) == 57:
return cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(
data
)
if len(data) == 32:
if self.type == "X25519":
return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(
data
)
if self.type == "Ed25519":
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(
data
)
try:
return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(
data
)
except Exception:
return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(
data
)
raise PrivateKeyError("Cannot load raw key")
return cryptography.hazmat.primitives.serialization.load_pem_private_key(
data,
None if self.passphrase is None else to_bytes(self.passphrase),
)
except Exception as e:
raise PrivateKeyError(e) from e
def _ensure_existing_private_key_loaded(self) -> None:
if self.existing_private_key is None and self.has_existing():
self.existing_private_key = self._load_privatekey()
def _check_passphrase(self) -> bool:
if self.existing_private_key_bytes is None:
raise AssertionError("existing_private_key_bytes not set")
try:
key_format = identify_private_key_format(self.existing_private_key_bytes)
if key_format == "raw":
# Raw keys cannot be encrypted. To avoid incompatibilities, we try to
# actually load the key (and return False when this fails).
self._load_privatekey()
# Loading the key succeeded. Only return True when no passphrase was
# provided.
return self.passphrase is None
return bool(
cryptography.hazmat.primitives.serialization.load_pem_private_key(
self.existing_private_key_bytes,
None if self.passphrase is None else to_bytes(self.passphrase),
)
)
except Exception:
return False
def _check_size_and_type(self) -> bool:
if isinstance(
self.existing_private_key,
cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey,
):
return (
self.type == "RSA" and self.size == self.existing_private_key.key_size
)
if isinstance(
self.existing_private_key,
cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey,
):
return (
self.type == "DSA" and self.size == self.existing_private_key.key_size
)
if isinstance(
self.existing_private_key,
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey,
):
return self.type == "X25519"
if isinstance(
self.existing_private_key,
cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey,
):
return self.type == "X448"
if isinstance(
self.existing_private_key,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey,
):
return self.type == "Ed25519"
if 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
if self.curve not in self.curves:
return False
return self.curves[self.curve].verify(
privatekey=self.existing_private_key, module=self.module
)
return False
def _check_format(self) -> bool:
if self.existing_private_key_bytes is None:
raise AssertionError("existing_private_key_bytes not set")
if self.format == "auto_ignore":
return True
try:
key_format = identify_private_key_format(self.existing_private_key_bytes)
return key_format == self._get_wanted_format()
except Exception:
return False
def select_backend(module: GeneralAnsibleModule) -> PrivateKeyBackend:
assert_required_cryptography_version(
module, minimum_cryptography_version=MINIMAL_CRYPTOGRAPHY_VERSION
)
return PrivateKeyCryptographyBackend(module=module)
def get_privatekey_argument_spec() -> ArgumentSpec:
return ArgumentSpec(
argument_spec={
"size": {"type": "int", "default": 4096},
"type": {
"type": "str",
"default": "RSA",
"choices": ["DSA", "ECC", "Ed25519", "Ed448", "RSA", "X25519", "X448"],
},
"curve": {
"type": "str",
"choices": [
"secp224r1",
"secp256k1",
"secp256r1",
"secp384r1",
"secp521r1",
"secp192r1",
"brainpoolP256r1",
"brainpoolP384r1",
"brainpoolP512r1",
"sect163k1",
"sect163r2",
"sect233k1",
"sect233r1",
"sect283k1",
"sect283r1",
"sect409k1",
"sect409r1",
"sect571k1",
"sect571r1",
],
},
"passphrase": {"type": "str", "no_log": True},
"cipher": {"type": "str", "default": "auto"},
"format": {
"type": "str",
"default": "auto_ignore",
"choices": ["pkcs1", "pkcs8", "raw", "auto", "auto_ignore"],
},
"format_mismatch": {
"type": "str",
"default": "regenerate",
"choices": ["regenerate", "convert"],
},
"select_crypto_backend": {
"type": "str",
"choices": ["auto", "cryptography"],
"default": "auto",
},
"regenerate": {
"type": "str",
"default": "full_idempotence",
"choices": [
"never",
"fail",
"partial_idempotence",
"full_idempotence",
"always",
],
},
},
required_if=[
("type", "ECC", ["curve"]),
],
)
__all__ = (
"PrivateKeyError",
"PrivateKeyBackend",
"select_backend",
"get_privatekey_argument_spec",
)

View File

@@ -0,0 +1,313 @@
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import abc
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils._argspec import (
ArgumentSpec,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
cryptography_compare_private_keys,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.pem import (
identify_private_key_format,
)
from ansible_collections.community.crypto.plugins.module_utils._cryptography_dep import (
COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION,
assert_required_cryptography_version,
)
from ansible_collections.community.crypto.plugins.module_utils._io import load_file
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
from cryptography.hazmat.primitives.asymmetric.types import (
PrivateKeyTypes,
)
MINIMAL_CRYPTOGRAPHY_VERSION = COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION
try:
import cryptography
import cryptography.exceptions
import cryptography.hazmat.backends
import cryptography.hazmat.primitives.asymmetric.dsa
import cryptography.hazmat.primitives.asymmetric.ec
import cryptography.hazmat.primitives.asymmetric.ed448
import cryptography.hazmat.primitives.asymmetric.ed25519
import cryptography.hazmat.primitives.asymmetric.rsa
import cryptography.hazmat.primitives.asymmetric.utils
import cryptography.hazmat.primitives.asymmetric.x448
import cryptography.hazmat.primitives.asymmetric.x25519
import cryptography.hazmat.primitives.serialization
except ImportError:
pass
class PrivateKeyError(OpenSSLObjectError):
pass
# From the object called `module`, only the following properties are used:
#
# - module.params[]
# - module.warn(msg: str)
# - module.fail_json(msg: str, **kwargs)
class PrivateKeyConvertBackend(metaclass=abc.ABCMeta):
def __init__(self, *, module: AnsibleModule) -> None:
self.module = module
self.src_path: str | None = module.params["src_path"]
self.src_content: str | None = module.params["src_content"]
self.src_passphrase: str | None = module.params["src_passphrase"]
self.format: t.Literal["pkcs1", "pkcs8", "raw"] = module.params["format"]
self.dest_passphrase: str | None = module.params["dest_passphrase"]
self.src_private_key: PrivateKeyTypes | None = None
if self.src_path is not None:
self.src_private_key_bytes = load_file(path=self.src_path, module=module)
else:
if self.src_content is None:
raise AssertionError("src_content is None")
self.src_private_key_bytes = self.src_content.encode("utf-8")
self.dest_private_key: PrivateKeyTypes | None = None
self.dest_private_key_bytes: bytes | None = None
@abc.abstractmethod
def get_private_key_data(self) -> bytes:
"""Return bytes for self.src_private_key in output format."""
def set_existing_destination(self, *, privatekey_bytes: bytes | None) -> None:
"""Set existing private key bytes. None indicates that the key does not exist."""
self.dest_private_key_bytes = privatekey_bytes
def has_existing_destination(self) -> bool:
"""Query whether an existing private key is/has been there."""
return self.dest_private_key_bytes is not None
@abc.abstractmethod
def _load_private_key(
self,
*,
data: bytes,
passphrase: str | None,
current_hint: PrivateKeyTypes | None = None,
) -> tuple[str, PrivateKeyTypes]:
"""Check whether data can be loaded as a private key with the provided passphrase. Return tuple (type, private_key)."""
def needs_conversion(self) -> bool:
"""Check whether a conversion is necessary. Must only be called if needs_regeneration() returned False."""
dummy, self.src_private_key = self._load_private_key(
data=self.src_private_key_bytes, passphrase=self.src_passphrase
)
if not self.has_existing_destination():
return True
assert self.dest_private_key_bytes is not None
try:
key_format, self.dest_private_key = self._load_private_key(
data=self.dest_private_key_bytes,
passphrase=self.dest_passphrase,
current_hint=self.src_private_key,
)
except Exception:
return True
return key_format != self.format or not cryptography_compare_private_keys(
self.dest_private_key, self.src_private_key
)
def dump(self) -> dict[str, t.Any]:
"""Serialize the object into a dictionary."""
return {}
# Implementation with using cryptography
class PrivateKeyConvertCryptographyBackend(PrivateKeyConvertBackend):
def __init__(self, *, module: AnsibleModule) -> None:
super().__init__(module=module)
def get_private_key_data(self) -> bytes:
"""Return bytes for self.src_private_key in output format"""
if self.src_private_key is None:
raise AssertionError("src_private_key not set")
# Select export format and encoding
try:
export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM
if self.format == "pkcs1":
# "TraditionalOpenSSL" format is PKCS1
export_format = (
cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL
)
elif self.format == "pkcs8":
export_format = (
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
)
else:
# pylint does not notice that all possible values for self.format have been covered.
raise AssertionError("Can never be reached") # pragma: no cover
except AttributeError:
self.module.fail_json(
msg=f'Cryptography backend does not support the selected output format "{self.format}"'
)
# Select key encryption
encryption_algorithm: (
cryptography.hazmat.primitives.serialization.KeySerializationEncryption
) = cryptography.hazmat.primitives.serialization.NoEncryption()
if self.dest_passphrase:
encryption_algorithm = (
cryptography.hazmat.primitives.serialization.BestAvailableEncryption(
to_bytes(self.dest_passphrase)
)
)
# Serialize key
try:
return self.src_private_key.private_bytes(
encoding=export_encoding,
format=export_format,
encryption_algorithm=encryption_algorithm,
)
except ValueError:
self.module.fail_json(
msg=f'Cryptography backend cannot serialize the private key in the required format "{self.format}"'
)
except Exception:
self.module.fail_json(
msg=f'Error while serializing the private key in the required format "{self.format}"',
exception=traceback.format_exc(),
)
def _load_private_key(
self,
*,
data: bytes,
passphrase: str | None,
current_hint: PrivateKeyTypes | None = None,
) -> tuple[str, PrivateKeyTypes]:
try:
# Interpret bytes depending on format.
key_format = identify_private_key_format(data)
if key_format == "raw":
if passphrase is not None:
raise PrivateKeyError("Cannot load raw key with passphrase")
if len(data) == 56:
return (
key_format,
cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(
data
),
)
if len(data) == 57:
return (
key_format,
cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(
data
),
)
if len(data) == 32:
if isinstance(
current_hint,
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey,
):
try:
return (
key_format,
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(
data
),
)
except Exception:
return (
key_format,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(
data
),
)
else:
try:
return (
key_format,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(
data
),
)
except Exception:
return (
key_format,
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(
data
),
)
raise PrivateKeyError("Cannot load raw key")
return (
key_format,
cryptography.hazmat.primitives.serialization.load_pem_private_key(
data,
None if passphrase is None else to_bytes(passphrase),
),
)
except Exception as e:
raise PrivateKeyError(e) from e
def select_backend(module: AnsibleModule) -> PrivateKeyConvertBackend:
assert_required_cryptography_version(
module, minimum_cryptography_version=MINIMAL_CRYPTOGRAPHY_VERSION
)
return PrivateKeyConvertCryptographyBackend(module=module)
def get_privatekey_argument_spec() -> ArgumentSpec:
return ArgumentSpec(
argument_spec={
"src_path": {"type": "path"},
"src_content": {"type": "str"},
"src_passphrase": {"type": "str", "no_log": True},
"dest_passphrase": {"type": "str", "no_log": True},
"format": {
"type": "str",
"required": True,
"choices": ["pkcs1", "pkcs8", "raw"],
},
},
mutually_exclusive=[
["src_path", "src_content"],
],
required_one_of=[
["src_path", "src_content"],
],
)
__all__ = (
"PrivateKeyError",
"PrivateKeyConvertBackend",
"select_backend",
"get_privatekey_argument_spec",
)

View File

@@ -0,0 +1,369 @@
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see 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
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!
from __future__ import annotations
import abc
import typing as t
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.math import (
binary_exp_mod,
quick_is_not_prime,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.publickey_info import (
_get_cryptography_public_key_info,
)
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._cryptography_dep import (
COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION,
assert_required_cryptography_version,
)
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.plugin_utils._action_module import (
AnsibleActionModule,
)
from ansible_collections.community.crypto.plugins.plugin_utils._filter_module import (
FilterModuleMock,
)
from cryptography.hazmat.primitives.asymmetric.types import (
PrivateKeyTypes,
)
GeneralAnsibleModule = t.Union[AnsibleModule, AnsibleActionModule, FilterModuleMock]
MINIMAL_CRYPTOGRAPHY_VERSION = COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION
try:
import cryptography
from cryptography.hazmat.primitives import serialization
except ImportError:
pass
SIGNATURE_TEST_DATA = b"1234"
def _get_cryptography_private_key_info(
key: PrivateKeyTypes, *, need_private_key_data: bool = False
) -> tuple[str, dict[str, t.Any], dict[str, t.Any]]:
key_type, key_public_data = _get_cryptography_public_key_info(key.public_key())
key_private_data: dict[str, t.Any] = {}
if need_private_key_data:
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
rsa_private_numbers = key.private_numbers()
key_private_data["p"] = rsa_private_numbers.p
key_private_data["q"] = rsa_private_numbers.q
key_private_data["exponent"] = rsa_private_numbers.d
elif isinstance(
key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey
):
dsa_private_numbers = key.private_numbers()
key_private_data["x"] = dsa_private_numbers.x
elif isinstance(
key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey
):
ecc_private_numbers = key.private_numbers()
key_private_data["multiplier"] = ecc_private_numbers.private_value
return key_type, key_public_data, key_private_data
def _check_dsa_consistency(
*, key_public_data: dict[str, t.Any], key_private_data: dict[str, t.Any]
) -> bool | None:
# Get parameters
p: int | None = key_public_data.get("p")
if p is None:
return None
q: int | None = key_public_data.get("q")
if q is None:
return None
g: int | None = key_public_data.get("g")
if g is None:
return None
y: int | None = key_public_data.get("y")
if y is None:
return None
x: int | None = key_private_data.get("x")
if x is None:
return None
# Make sure that g is not 0, 1 or -1 in Z/pZ
if g < 2 or g >= p - 1:
return False
# Make sure that x is in range
if x < 1 or x >= q:
return False
# Check whether q divides p-1
if (p - 1) % q != 0:
return False
# Check that g**q mod p == 1
if binary_exp_mod(g, q, m=p) != 1:
return False
# Check whether g**x mod p == y
if binary_exp_mod(g, x, m=p) != y:
return False
# Check (quickly) whether p or q are not primes
if quick_is_not_prime(q) or quick_is_not_prime(p):
return False
return True
def _is_cryptography_key_consistent(
key: PrivateKeyTypes,
*,
key_public_data: dict[str, t.Any],
key_private_data: dict[str, t.Any],
warn_func: t.Callable[[str], None] | None = None,
) -> bool | None:
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
# key._backend was removed in cryptography 42.0.0
backend = getattr(key, "_backend", None)
if backend is not None:
return bool(backend._lib.RSA_check_key(key._rsa_cdata)) # type: ignore # pylint: disable=protected-access
if isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
result = _check_dsa_consistency(
key_public_data=key_public_data, key_private_data=key_private_data
)
if result is not None:
return result
signature = key.sign(
SIGNATURE_TEST_DATA, cryptography.hazmat.primitives.hashes.SHA256()
)
try:
key.public_key().verify(
signature,
SIGNATURE_TEST_DATA,
cryptography.hazmat.primitives.hashes.SHA256(),
)
return True
except cryptography.exceptions.InvalidSignature:
return False
if isinstance(
key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey
):
signature = key.sign(
SIGNATURE_TEST_DATA,
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(
cryptography.hazmat.primitives.hashes.SHA256()
),
)
try:
key.public_key().verify(
signature,
SIGNATURE_TEST_DATA,
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(
cryptography.hazmat.primitives.hashes.SHA256()
),
)
return True
except cryptography.exceptions.InvalidSignature:
return False
has_simple_sign_function = False
if isinstance(
key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
):
has_simple_sign_function = True
if isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
has_simple_sign_function = True
if has_simple_sign_function:
signature = key.sign(SIGNATURE_TEST_DATA) # type: ignore
try:
key.public_key().verify(signature, SIGNATURE_TEST_DATA) # type: ignore
return True
except cryptography.exceptions.InvalidSignature:
return False
# For X25519 and X448, there's no test yet.
if warn_func is not None:
warn_func(f"Cannot determine consistency for key of type {type(key)}")
return None
class PrivateKeyConsistencyError(OpenSSLObjectError):
def __init__(self, msg: str, *, result: dict[str, t.Any]) -> None:
super().__init__(msg)
self.error_message = msg
self.result = result
class PrivateKeyParseError(OpenSSLObjectError):
def __init__(self, msg: str, *, result: dict[str, t.Any]) -> None:
super().__init__(msg)
self.error_message = msg
self.result = result
class PrivateKeyInfoRetrieval(metaclass=abc.ABCMeta):
key: PrivateKeyTypes
def __init__(
self,
*,
module: GeneralAnsibleModule,
content: bytes,
passphrase: str | None = None,
return_private_key_data: bool = False,
check_consistency: bool = False,
):
self.module = module
self.content = content
self.passphrase = passphrase
self.return_private_key_data = return_private_key_data
self.check_consistency = check_consistency
@abc.abstractmethod
def _get_public_key(self, *, binary: bool) -> bytes:
pass
@abc.abstractmethod
def _get_key_info(
self, *, need_private_key_data: bool = False
) -> tuple[str, dict[str, t.Any], dict[str, t.Any]]:
pass
@abc.abstractmethod
def _is_key_consistent(
self, *, key_public_data: dict[str, t.Any], key_private_data: dict[str, t.Any]
) -> bool | None:
pass
def get_info(self, *, prefer_one_fingerprint: bool = False) -> dict[str, t.Any]:
result: dict[str, t.Any] = {
"can_parse_key": False,
"key_is_consistent": None,
}
priv_key_detail = self.content
try:
self.key = load_privatekey(
path=None,
content=priv_key_detail,
passphrase=(
to_bytes(self.passphrase)
if self.passphrase is not None
else self.passphrase
),
)
result["can_parse_key"] = True
except OpenSSLObjectError as exc:
raise PrivateKeyParseError(str(exc), result=result) from exc
result["public_key"] = to_text(self._get_public_key(binary=False))
pk = self._get_public_key(binary=True)
result["public_key_fingerprints"] = (
get_fingerprint_of_bytes(pk, prefer_one=prefer_one_fingerprint)
if pk is not None
else {}
)
key_type, key_public_data, key_private_data = self._get_key_info(
need_private_key_data=self.return_private_key_data or self.check_consistency
)
result["type"] = key_type
result["public_data"] = key_public_data
if self.return_private_key_data:
result["private_data"] = key_private_data
if self.check_consistency:
result["key_is_consistent"] = self._is_key_consistent(
key_public_data=key_public_data, key_private_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")
msg = (
"Private key is not consistent! (See "
"https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)"
)
raise PrivateKeyConsistencyError(msg, result=result)
return result
class PrivateKeyInfoRetrievalCryptography(PrivateKeyInfoRetrieval):
"""Validate the supplied private key, using the cryptography backend"""
def __init__(
self, *, module: GeneralAnsibleModule, content: bytes, **kwargs
) -> None:
super().__init__(module=module, content=content, **kwargs)
def _get_public_key(self, *, binary: bool) -> bytes:
return self.key.public_key().public_bytes(
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo,
)
def _get_key_info(
self, *, need_private_key_data: bool = False
) -> tuple[str, dict[str, t.Any], dict[str, t.Any]]:
return _get_cryptography_private_key_info(
self.key, need_private_key_data=need_private_key_data
)
def _is_key_consistent(
self, *, key_public_data: dict[str, t.Any], key_private_data: dict[str, t.Any]
) -> bool | None:
return _is_cryptography_key_consistent(
self.key,
key_public_data=key_public_data,
key_private_data=key_private_data,
warn_func=self.module.warn,
)
def get_privatekey_info(
*,
module: GeneralAnsibleModule,
content: bytes,
passphrase: str | None = None,
return_private_key_data: bool = False,
prefer_one_fingerprint: bool = False,
) -> dict[str, t.Any]:
info = PrivateKeyInfoRetrievalCryptography(
module=module,
content=content,
passphrase=passphrase,
return_private_key_data=return_private_key_data,
)
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
def select_backend(
*,
module: GeneralAnsibleModule,
content: bytes,
passphrase: str | None = None,
return_private_key_data: bool = False,
check_consistency: bool = False,
) -> PrivateKeyInfoRetrieval:
assert_required_cryptography_version(
module, minimum_cryptography_version=MINIMAL_CRYPTOGRAPHY_VERSION
)
return PrivateKeyInfoRetrievalCryptography(
module=module,
content=content,
passphrase=passphrase,
return_private_key_data=return_private_key_data,
check_consistency=check_consistency,
)
__all__ = (
"PrivateKeyConsistencyError",
"PrivateKeyParseError",
"PrivateKeyInfoRetrieval",
"get_privatekey_info",
"select_backend",
)

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