Compare commits

...

122 Commits

Author SHA1 Message Date
Felix Fontein
9289ce8534 Release 2.3.2. 2022-06-02 12:28:12 +02:00
Felix Fontein
798d12b499 Revert "Deprecate Ansible 2.9 and ansible-base 2.10 support. (#460)"
(This is so we can do a bugfix release now. Deprecations are not alloewd in bugfix releases.)

This reverts commit 74960eaeac.
2022-06-02 07:49:28 +02:00
Felix Fontein
5146760def Adjust release summary, since there are other fragments. 2022-06-02 07:49:16 +02:00
Felix Fontein
48649d50b5 Prepare 2.3.2 release. 2022-06-02 07:46:42 +02:00
Felix Fontein
ccd66419f4 Add simplified_bsd.txt license file (#467)
* Add simplified_bsd.txt and adjust references.

* Add changelog.
2022-06-02 07:45:50 +02:00
Felix Fontein
80334884fb Update CI matrix. (#464) 2022-05-27 11:00:01 +02:00
Andrew Pantuso
4ab45e8c21 ci: enable rhel9.0 tests for openssh_cert (#463)
* ci: enable rhel9.0 tests for openssh_cert

* ci: allow openssh_cert second signature algorithm test for versions >8.7

* ci: narrowing condition to not attempt RSA1 signing exclusively on RHEL >=9

* ci: grouping and documenting condition
2022-05-21 16:43:54 +02:00
Felix Fontein
c566a7abf3 Add RHEL 9.0, FreeBSD 13.1, Ubuntu 22.04 and Fedora 36 to CI (#456)
* Add RHEL 9.0 and FreeBSD 13.1 to CI.

* Add Ubuntu 22.04 and Fedora 36 to CI.

* Switch orders so that root doesn't have a SHA1 signature.

* Skip openssh_cert test on RHEL 9.0.

* Make it possible that pyOpenSSL isn't installed *at all*.

* Work with default.
2022-05-20 23:03:54 +02:00
Felix Fontein
fd0048827d Remove FreeBSD 12.1 from CI. The remote seems to be no longer working. (#459) 2022-05-20 14:47:05 +02:00
Felix Fontein
74960eaeac Deprecate Ansible 2.9 and ansible-base 2.10 support. (#460) 2022-05-20 14:45:56 +02:00
Felix Fontein
c49102d688 certificate_complete_chain: do not stop execution on unsupported algorithm (#457)
* Do not stop execution on unsupported algorithm.

* Fix typo.
2022-05-20 07:22:35 +02:00
Felix Fontein
40cf8ba2ce Next expected release is 2.4.0. 2022-05-16 13:02:15 +02:00
Felix Fontein
ffc0ab2d40 Release 2.3.1. 2022-05-16 12:27:29 +02:00
Felix Fontein
7183596586 Update with actual CPython 3.9.5 license. (#454) 2022-05-16 08:24:43 +02:00
Felix Fontein
5664bfe4b6 Add PSF-license.txt file. (#453) 2022-05-16 07:40:04 +02:00
Felix Fontein
4074ff4132 Prepare 2.3.1 release. 2022-05-16 07:13:15 +02:00
Felix Fontein
e7f9885aa3 Next expected release is 2.4.0. 2022-05-09 21:11:07 +02:00
Felix Fontein
5641e2ac9b Release 2.3.0. 2022-05-09 20:53:39 +02:00
Felix Fontein
4cf951596f Improve handling of IDNA/Unicode domains (#436)
* Prepare IDNA/Unicode conversion code. Use to normalize input.

* Use IDNA library first (IDNA2008) and Python's IDNA2003 implementation as a fallback.

* Make sure idna is installed.

* Add changelog fragment.

* 'punycode' → 'idna'.

* Add name_encoding options and tests.

* Avoid invalid character for IDNA2008.

* Linting.

* Forgot to upate value.

* Work around cryptography bug. Fix port handling for URIs.

* Forgot other place sensitive to cryptography bug.

* Forgot one. (Will likely still fail.)

* Decode IDNA in _compress_entry() to avoid comparison screw-ups.

* Work around Python 3.5 problem in Ansible 2.9's default test container.

* Update changelog fragment.

* Fix error, add tests.

* Python 2 compatibility.

* Update requirements.
2022-05-09 19:57:14 +02:00
Felix Fontein
90efcc1ca7 Add privatekey_content option. (#452) 2022-05-09 19:56:08 +02:00
Felix Fontein
8a1c60e54a Incorporate feedback from #443 that ACME modules work with Sectigo ACME Service for InCommon (#451)
* Incorporate feedback from #443.

* Update comment.
2022-05-09 13:28:03 +02:00
Felix Fontein
43665a3892 Prepare 2.3.0 release. 2022-05-03 19:24:20 +02:00
Felix Fontein
640bdbc066 Add EE support (#440)
* Add EE files.

* Install cryptography and PyOpenSSL from PyPi.

* Revert "Install cryptography and PyOpenSSL from PyPi."

This reverts commit 6b90a1efae.

* Only run test when cryptography has a new enough version.

* And another one.

* Extend changelog.
2022-05-03 19:22:55 +02:00
Jonas Verhofsté
c16d9f78b8 Make request timeout configurable for all acme modules (#448)
* Make request timeout configurable for all acme modules

Fixes #447.

* Log change made in #448
2022-05-03 17:29:38 +02:00
Felix Fontein
91f192ce5b Fix main for new cryptography 37.0.0 release (#445)
* Fix empty check for openssl_pkcs12 tests.

* Remove unnecessary imports.

* Prevent crash if PyOpenSSL cannot be imported because of an AttributeError.

* Add changelog fragment.

* Fix constraints file.

* Use Python 2.7 instead of 3.5 for 2.9 cloud tests (pip module is broken).

* Prevent upgrading cryptography on ansible-core 2.12's default container with Python 3.9.
2022-04-26 22:18:37 +02:00
Felix Fontein
e560acdac5 Remove distutils from unit tests. (#444) 2022-04-21 11:05:18 +02:00
Felix Fontein
9d03178b00 Fix crash in x509_crl when certificate issuer is specified (#441)
* Fix x509_crl certificate issuer issue.

* Add tests.

* Add changelog fragment.
2022-04-18 08:17:27 +02:00
Yauhen
041fff5057 Add persistent and perf options to the luks_device (#434)
Read and write work queue significantly degrades performance on
SSD/NVME devices[1].

In Debian 11 crypttab does not support no-read-workqueue and
no-write-workqueue flags, so the persistent flag is workaround: once
opened with perf parameters persists forever.

[1] https://blog.cloudflare.com/speeding-up-linux-disk-encryption/

Signed-off-by: Yauhen Artsiukhou <jsirex@gmail.com>
2022-04-10 14:30:10 +02:00
Felix Fontein
c7f581daad Switch from antsibull to antsibull-docs. (#438) 2022-04-10 10:43:36 +02:00
Felix Fontein
b47168994d Remove OpenSuSE Python 2 from devel CI. (#435) 2022-04-02 18:09:09 +02:00
bluikko
635b25519b Document OpenSSH 7.7 required for validity always (#429)
* Document OpenSSH 7.7 required for validity always

* Document cert start time option for OpenSSH <7.7

* Improve language of "always" time support

* Update language on `always` to suggested

* Fix indetation
2022-04-02 12:02:42 -04:00
Felix Fontein
bc00c30faf Replace antsibull-lint collection-docs with antsibull-docs lint-collection-docs. (#432) 2022-03-30 08:00:14 +02:00
Felix Fontein
a28b2a5b4b Add stable-2.13 to CI, thin out older version matrix (#425)
* Add stable-2.13 to CI, thin out older version matrix.

* Thin out a bit more.
2022-03-29 06:49:38 +02:00
Felix Fontein
9e611a6148 Next expected release is 2.3.0. 2022-03-22 13:44:19 +01:00
Felix Fontein
025091c3fb Release 2.2.4 2022-03-22 13:17:36 +01:00
Felix Fontein
b0cede8231 Prepare 2.2.4 release. 2022-03-22 07:31:54 +01:00
Felix Fontein
22084b26d4 Add collection links file. (#421) 2022-03-22 07:04:23 +01:00
Felix Fontein
867158a942 Run ACME tests on more targets. (#419) 2022-03-12 08:55:06 +01:00
Andrew Pantuso
033bab7db1 openssh_* - catch and report top-level exceptions via fail_json (#417)
* ensure exceptions are properly reported

* adding changelog fragment

* applying review suggestions

* typo

* adding back exception msg
2022-03-08 13:23:09 +01:00
Felix Fontein
47d9fad45f Next expected release is 2.3.0. 2022-03-04 08:12:32 +01:00
Felix Fontein
f82dcbea21 Release 2.2.3. 2022-03-04 07:38:44 +01:00
Felix Fontein
67f511b5ad Prepare 2.2.3 release. 2022-03-03 21:18:07 +01:00
Andrew Pantuso
010f1a4d2d fixing public key return value docs (#412) 2022-03-02 13:52:07 +01:00
Felix Fontein
0d4b3ed991 Fix parsing of lsblk output. (#410) 2022-03-02 13:48:38 +01:00
Felix Fontein
2f2c8b57e8 Next expected release is 2.3.0. 2022-02-21 22:41:39 +01:00
Felix Fontein
b952b103e2 Releasing 2.2.2. 2022-02-21 22:11:55 +01:00
Felix Fontein
c85659ebfc Prepare 2.2.2 release. 2022-02-19 18:55:41 +01:00
Felix Fontein
73c8577b61 Integrate Alpine into CI (#408)
* Integrate Alpine into CI.

* Fix package names.
2022-02-19 17:54:05 +00:00
Felix Fontein
84c1a20af7 CI: add community ansible-test images (#404)
* Use community ansible-test images.

* Adjust tests for new operating systems, and pass on Python version as well.

* Fix Python version.

Co-authored-by: David Moreau Simard <moi@dmsimard.com>

* Fix package name.

Co-authored-by: David Moreau Simard <moi@dmsimard.com>
2022-02-17 22:29:50 +01:00
Felix Fontein
28729657ac x509_certificate: check existing certificate's signature for selfsigned and ownca provider (#407)
* Verify whether signature matches.

* Add changelog fragment.

* Forgot imports.

* Fix wrong name.

* Check whether the CA private key fits to the CA certificate. Use correct key in tests.

* Refactor code.
2022-02-16 07:38:11 +01:00
Felix Fontein
3ebc132c03 Regenerate certificate on CA's subject change. (#402) 2022-02-14 18:04:29 +01:00
Felix Fontein
11a14543c8 certificate_complete_chain: handle duplicate intermediate subjects (#403)
* Allow multiple intermediate CAs to have same subject.

* Add tests.

* Fix test name.

* Don't use CN for SAN.

* Make a bit more compatible.

* Include jinja2 compat for CentOS 6.
2022-02-14 13:29:19 +01:00
Felix Fontein
7e92f04c93 Next expected release is 2.3.0. 2022-02-05 21:51:19 +01:00
Felix Fontein
8a74b3e259 Release 2.2.1. 2022-02-05 21:28:47 +01:00
Felix Fontein
a9fcd584e9 Prepare 2.2.1 release. 2022-02-05 20:18:32 +01:00
Andrew Pantuso
a307618872 openssh_cert - fix full_idempotence for host certificates (#396)
* fixing host cert idempotence

* adding changelog fragment
2022-02-04 20:53:50 +01:00
Felix Fontein
12749088a0 Next expected release is 2.3.0. 2022-02-01 06:25:49 +01:00
Felix Fontein
90da233341 Release 2.2.0. 2022-02-01 05:49:27 +01:00
Felix Fontein
23226dce8f Drop CentOS 8 from CI. (#393) 2022-01-31 21:17:41 +00:00
Felix Fontein
9faed1dad0 Prepare 2.2.0 release. 2022-01-31 06:02:48 +01:00
Felix Fontein
ea2e45d63f Set LANG and similar env variables to prevent translated cryptsetup output. (#388) 2022-01-30 21:30:56 +01:00
Felix Fontein
5abfe8fca9 PyOpenSSL 22.0.0 no longer supports Python 2.7. (#387) 2022-01-30 13:59:32 +01:00
JochenKorge
b339e71973 Added 'ignore_timestamps' parameter (#381)
* Added 'ignore_timestamps' parameter

* Update plugins/modules/openssh_cert.py

Co-authored-by: Andrew Pantuso <ajpantuso@gmail.com>

* Update plugins/modules/openssh_cert.py

Co-authored-by: Andrew Pantuso <ajpantuso@gmail.com>

* Update plugins/modules/openssh_cert.py

Co-authored-by: Andrew Pantuso <ajpantuso@gmail.com>

* Added fragment

* Update plugins/modules/openssh_cert.py

Co-authored-by: Andrew Pantuso <ajpantuso@gmail.com>

* added ignore_timestamps to example

* corrected styling

* fixed styling (again)

* Update changelogs/fragments/381_openssh_cert_add_ignore_timestamps.yml

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

* splitted description as suggested by felixfontein

* fixed linebreak

* Mentioned ignore_timestamps in regenerate

Co-authored-by: Andrew Pantuso <ajpantuso@gmail.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
2022-01-20 16:15:50 +01:00
Felix Fontein
a467f036b1 Fix indentation of when in example. (#382) 2022-01-18 21:52:53 +01:00
Felix Fontein
cd5ed011a5 Update CI matrix for Remote Devel (#377)
* Update CI matrix for Remote Devel.

* Add Python info entries.
2022-01-13 09:18:48 +01:00
Felix Fontein
9dfdaaf471 Next expected release is 2.2.0. 2022-01-10 23:03:02 +01:00
Felix Fontein
950d1d072f Release 2.1.0. 2022-01-10 22:48:03 +01:00
Felix Fontein
cb14e73c61 Prepare 2.1.0 release. 2022-01-10 21:03:19 +01:00
Felix Fontein
bd2bd79497 Add openssl_privatekey_convert module (#362)
* Add openssl_privatekey_convert module.

* Extend tests and fix bugs.

* Fix wrong required.

* Fix condition.

* Fix bad tests.

* Fix documentation for format.

* Fix copyright lines.
2022-01-10 21:01:52 +01:00
Felix Fontein
62272296da Small docs improvements (#374)
* Small improvements.

* Document behavior changes.
2022-01-10 12:05:09 +00:00
Felix Fontein
1b0fcde862 Fix comment. (#372) 2022-01-06 12:56:10 +00:00
Felix Fontein
46f39efc43 Use vendored copy of distutils.version. (#369) 2022-01-05 21:30:11 +01:00
Felix Fontein
b2ea4a7ce5 Add basic crypto_info module (#363)
* Add basic crypto_info module.

* Improve check.

* Actually test capabilities.

* Also output EC curve list.

* Fix detections.

* Ed25519 and Ed448 are not supported on FreeBSD 12.1.

* Refactor.

* Also retrieve information on the OpenSSL binary.

* Improve splitting.

* Update plugins/modules/crypto_info.py

Co-authored-by: Andrew Pantuso <ajpantuso@gmail.com>

* Replace list by tuple.

Co-authored-by: Andrew Pantuso <ajpantuso@gmail.com>
2022-01-05 18:19:42 +01:00
Felix Fontein
3e307fe062 Fix typo. (#367) 2022-01-05 18:16:27 +01:00
Felix Fontein
6ee238d961 certificate_complete_chain: avoid infinite loops, and double roots when root certificate was already part of chain (#360)
* Avoid infinite loops, and double roots when root certificate was already part of chain.

* Refactor tests for readability.
2022-01-04 07:00:09 +01:00
Felix Fontein
f3e431912d Fix indentation in docs. (#364) 2022-01-03 21:38:37 +01:00
Felix Fontein
471506c5d4 Improve changed / nonchanged validations by using new modules from community.internal_test_tools (#183)
* Use modules from internal_test_tools instead of stat workaround to check whether file actually changed.

* Properly add testing dependency.
2022-01-03 18:43:17 +01:00
Jens Heinrich
2c05221d89 Feature/rename test cases (#356)
* Name test tasks in a more explicite manner

* Space test + verification blocks apart

* Apply suggestions from code review

Co-authored-by: Jens Heinrich <github.com/JensHeinrich>
Co-authored-by: Felix Fontein <felix@fontein.de>
2021-12-30 10:06:43 +01:00
Felix Fontein
a539cd6939 Prepare for distutils.version being removed in Python 3.12 (#353)
* Prepare for distutils.version being removed in Python 2.12.

* Fix copy'n'paste error.

* Re-add Loose prefix.

* Fix Python version typo.

* Improve formulation.

* Move message into own line.

* Fix casing, now that the object is no longer called Version.
2021-12-24 11:28:14 +01:00
Felix Fontein
11c3974b0f Next expected release is 2.1.0. 2021-12-20 20:37:59 +01:00
Felix Fontein
605cf2631e Release 2.0.2. 2021-12-20 20:20:32 +01:00
Felix Fontein
ee196fd8a3 Prepare 2.0.2 release. 2021-12-20 20:15:27 +01:00
Jasmine Hegman
45b7aa797e Fix module reference in example (#351)
openssl_privatekey -> openssl_publickey
2021-12-13 06:48:59 +01:00
Bruno Vernay
32dab841d7 Fix CSR copy/paste error (#349)
The first case about ca_csr has been copy/pasted.
But in the following cases, the CSR must be the certificate csr.
2021-12-09 20:52:18 +01:00
Felix Fontein
5b1816719f Next expected release is 2.1.0. 2021-11-22 12:19:10 +01:00
Felix Fontein
bd2270fb05 Release 2.0.1. 2021-11-22 11:41:56 +01:00
Felix Fontein
3f40795a98 Extension parsing: add new fallback code which uses the new cryptography API (#331)
* Add new code as fallback which re-serializes de-serialized extensions using the new cryptography API.

* Forgot Base64 encoding.

* Add extension by OID tests.

* There's one value which is different with the new code.

* Differences in CI.

* Working around older Jinjas.

* Value depends on which SAN was included.

* Force complete CI run now since cryptography 36.0.0 is out.

ci_complete
2021-11-22 07:42:49 +01:00
Felix Fontein
73bc0f5de7 Use new PKCS#12 deserialization code from cryptography 36.0.0 if available (#302)
* Use new PKCS#12 deserialization code from cryptography 36.0.0 if available.

* Refactor into smaller functions.

* Force complete CI run now since cryptography 36.0.0 is out.

ci_complete
2021-11-22 07:41:54 +01:00
Felix Fontein
f832c0a4ac Fix missing s. 2021-11-22 07:41:39 +01:00
Felix Fontein
10579c8834 Prepare 2.0.1 release. 2021-11-22 07:40:23 +01:00
Felix Fontein
f1a6baadc7 Fix collection dependency installation in CI. (#341) 2021-11-18 20:42:57 +01:00
Felix Fontein
5de50b9f91 Fix compatibility to fetch_url change in ansible-core devel (#339)
* Fix compatibility to fetch_url change in ansible-core devel.

* Adjust tests.
2021-11-17 21:26:49 +01:00
Felix Fontein
cf0d2679aa Replace RHEL 8.4 by RHEL 8.5 for devel. (#337) 2021-11-17 07:18:08 +01:00
Felix Fontein
2d388bf8d0 This is no longer a problem with the dev version of cryptography. (#335) 2021-11-13 17:59:17 +01:00
Felix Fontein
056a86fcae Replace Bash codecov uploader by new Python codecov uploader. (#333)
ci_coverage
2021-11-13 12:06:10 +01:00
Felix Fontein
ebbfd7c56f luks_device: add built-in signature wiper to work around older wipefs versions with LUKS2 containers (#327)
* Use 'cryptsetup erase' to kill LUKS signature.

* Adjust unit test.

* Use own wiper for LUKS headers.

* Add comments.

* Fix tests.

* Update changelog.

* Remove 'cryptsetup erase'.

* Improve error messages.
2021-11-11 06:59:35 +01:00
Felix Fontein
91d98c4413 Replace Fedora 33 with Fedora 35 for devel tests. (#328) 2021-11-08 22:17:30 +01:00
Felix Fontein
51b6bb210d acme_certificate: fix crash when using fullchain_dest (#324)
* Fix crash when using fullchain_dest.

* Adjust changelog.

* Update plugins/module_utils/acme/backend_cryptography.py

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>
2021-11-05 08:51:43 +01:00
Felix Fontein
94634a347d Next expected release is 2.1.0. 2021-11-01 20:11:50 +01:00
Felix Fontein
e5acd27c9b Release 2.0.0. 2021-11-01 19:18:03 +01:00
Felix Fontein
e6cd66df53 Add release summary. (#319) 2021-11-01 18:53:46 +01:00
Felix Fontein
589e7c72ef Allow to specify subject (for CSRs) and issuer (for CRLs) ordered (#316)
* Allow to specify subject (for CSRs) and issuer (for CRLs) ordered.

* Forgot import.

* Apply suggestions from code review

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>

* Apply suggestions from code review

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>

* Fix typo.

* Simplify error handling, reject empty values outright.

* Document d497231e1c.

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>
2021-10-31 15:05:04 +01:00
Felix Fontein
ecbd44df22 Add warning for ASN.1 encoded extension values returned by some modules (#318)
* Add warning that ASN.1 encoded extension values returned by some modules might not reflect the exact byte sequence in the source file anymore depending on the cryptography version.

* Apply suggestions from code review

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>
2021-10-31 14:34:33 +01:00
Felix Fontein
4ab2ed8b77 Add ignore_timestamps option. (#317) 2021-10-30 16:34:27 +02:00
Felix Fontein
eb8dabce84 Improve Python 2 Unicode handling. (#313) 2021-10-22 07:15:20 +02:00
Felix Fontein
c5df302faa openssl_privatekey_info: disable private key consistency checks by default (#309)
* Disable private key consistency checks by default.

* Improve formulations, mention side-channel attacks.
2021-10-20 18:28:22 +02:00
Felix Fontein
a581f1ebcd Remove other deprecations (#290)
* Remove deprecated redirects.

* Remove deprecations.

* Add changelog fragment.

* Add some forgotten pieces.

* Bump version to 2.0.0.

* Fix formulation.
2021-10-16 21:00:48 +02:00
Felix Fontein
78b27ffedb Remove centos8 for devel from CI. (#307) 2021-10-16 08:33:29 +02:00
Felix Fontein
e735bdab60 Adjust to latest devel changes. 2021-10-12 19:16:46 +02:00
Felix Fontein
5f1efb6f7e Remove assertonly (#289)
* Remove assertonly backend.

* Remove assertonly tests.

* The expired test is basically a test of assertonly.

* Replace assertonly verification by _info + assert.
2021-10-10 10:24:00 +02:00
Felix Fontein
c68bfedbaa Fix shellcheck error. (#303) 2021-10-08 14:14:08 +02:00
Felix Fontein
871a185ecb Remove vendored copy of ipaddress (#287)
* Remove vendored copy of ipaddress.

* Forgot an import.

* Remove sanity ignores and checks related to ipaddress.

* Remove octal IPv4 address.

Such IPs are no longer accepted by ipaddress in Python's standard library (CVE-2021-29921).

* Remove unused import.

Co-authored-by: Sviatoslav Sydorenko <wk.cvs.github@sydorenko.org.ua>

Co-authored-by: Sviatoslav Sydorenko <wk.cvs.github@sydorenko.org.ua>
2021-10-06 14:53:40 +02:00
Felix Fontein
ed03841fd1 x509_certificate: document that *notBefore/*notAfter are not used for idempotency (#298)
* Document that *notBefore/*notAfter are not used for idempotency.

* Change formulation.
2021-10-03 22:03:58 +02:00
Felix Fontein
d6c0d53442 Fix PKCS#12 friendly name extraction for cryptography 35.0.0. (#296) 2021-10-03 20:25:24 +02:00
Felix Fontein
a2a7d94055 Support cryptography 35.0.0 for all modules except openssl_pkcs12 (#294)
* Add some workarounds for cryptography 35.0.0.

* Make fix work with very old cryptography versions as well (which supported multiple backends).

* [TEMP] Disable openssl_pkcs12 tests to see whether everything else works.

* Revert "[TEMP] Disable openssl_pkcs12 tests to see whether everything else works."

This reverts commit 3f905bc795.

* Add changelog fragment.

* Remove unnecessary assignment.

* Simplify code change.

* [TEMP] Disable openssl_pkcs12 tests to see whether everything else works.

* Revert "[TEMP] Disable openssl_pkcs12 tests to see whether everything else works."

This reverts commit fdb210528e.
2021-10-03 16:53:22 +02:00
Felix Fontein
2a7e452cf8 Ansible-core devel dropped support for Python 2.6. 2021-10-01 13:45:57 +02:00
Felix Fontein
74ae95038c Add patchback config. 2021-09-30 17:27:12 +02:00
Felix Fontein
57c364fe87 Temporarily fix CI for cryptography 35.0.0 release. (#292) 2021-09-30 13:39:34 +02:00
Felix Fontein
04958ece31 Deprecate ACME v1 (#288)
* Deprecate ACME v1.

* Fix syntax error.
2021-09-29 06:44:31 +02:00
Felix Fontein
838bdd711b Make Dirname (de)serialization conformant to RFC 4514 (#274)
* Adjust dirName serialization to RFC 4514.

* Adjust deserialization to RFC 4514.

* Add changelog fragment.

* Use Unicode strings, and work around Python 2 and Python 3 differences and problems with old cryptography versions.

* Work with bytes, not Unicode strings, to handle escaping of Unicode endpoints correctly.
2021-09-28 18:15:38 +02:00
Felix Fontein
f644db3c79 Remove PyOpenSSL backends (except for openssl_pkcs12) (#273)
* Remove Ubuntu 16.04 (Xenial Xerus) from CI.

* Removing PyOpenSSL backend from everywhere but openssl_pkcs12.

* Remove PyOpenSSL support from module_utils that's not needed for openssl_pkcs12.

* Add changelog fragment.
2021-09-28 17:46:35 +02:00
Felix Fontein
24e7d07973 Run CI on stable branches only once per week. 2021-09-28 17:34:30 +02:00
213 changed files with 6047 additions and 7799 deletions

View File

@@ -19,6 +19,11 @@ schedules:
branches:
include:
- main
- cron: 0 12 * * 0
displayName: Weekly (old stable branches)
always: true
branches:
include:
- stable-*
variables:
@@ -55,6 +60,17 @@ stages:
test: 'devel/sanity/extra'
- name: Units
test: 'devel/units/1'
- stage: Ansible_2_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: []
@@ -108,24 +124,36 @@ stages:
parameters:
testFormat: devel/linux/{0}/1
targets:
- name: CentOS 6
test: centos6
- name: CentOS 7
test: centos7
- name: CentOS 8
test: centos8
- name: Fedora 33
test: fedora33
- name: Fedora 34
test: fedora34
- name: openSUSE 15 py2
test: opensuse15py2
- name: openSUSE 15 py3
- name: Fedora 35
test: fedora35
- name: Fedora 36
test: fedora36
- name: openSUSE 15
test: opensuse15
- name: Ubuntu 18.04
test: ubuntu1804
- name: Ubuntu 20.04
test: ubuntu2004
- name: Ubuntu 22.04
test: ubuntu2204
- name: Alpine 3
test: alpine3
- stage: Docker_2_13
displayName: Docker 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.13/linux/{0}/1
targets:
- name: openSUSE 15 py2
test: opensuse15py2
- name: Fedora 34
test: fedora34
- name: Ubuntu 18.04
test: ubuntu1804
- name: Alpine 3
test: alpine3
- stage: Docker_2_12
displayName: Docker 2.12
dependsOn: []
@@ -134,14 +162,10 @@ stages:
parameters:
testFormat: 2.12/linux/{0}/1
targets:
- name: CentOS 8
test: centos8
- name: CentOS 6
test: centos6
- name: Fedora 33
test: fedora33
- name: openSUSE 15 py3
test: opensuse15
- name: Ubuntu 20.04
test: ubuntu2004
- stage: Docker_2_11
displayName: Docker 2.11
dependsOn: []
@@ -152,14 +176,10 @@ stages:
targets:
- name: CentOS 7
test: centos7
- name: CentOS 8
test: centos8
- name: Fedora 32
test: fedora32
- name: openSUSE 15 py2
test: opensuse15py2
- name: Ubuntu 18.04
test: ubuntu1804
- name: Alpine 3
test: alpine3
- stage: Docker_2_10
displayName: Docker 2.10
dependsOn: []
@@ -170,10 +190,6 @@ stages:
targets:
- name: CentOS 6
test: centos6
- name: Fedora 31
test: fedora31
- name: Ubuntu 16.04
test: ubuntu1604
- stage: Docker_2_9
displayName: Docker 2.9
dependsOn: []
@@ -182,17 +198,27 @@ stages:
parameters:
testFormat: 2.9/linux/{0}/1
targets:
- name: CentOS 6
test: centos6
- name: CentOS 7
test: centos7
- name: Fedora 31
test: fedora31
- name: Ubuntu 16.04
test: ubuntu1604
- name: Ubuntu 18.04
test: ubuntu1804
### Community Docker
- stage: Docker_community_devel
displayName: Docker (community images) devel
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: devel/linux-community/{0}/1
targets:
- name: Debian Bullseye
test: debian-bullseye/3.9
- name: ArchLinux
test: archlinux/3.10
- name: CentOS Stream 8
test: centos-stream8/3.8
### Remote
- stage: Remote_devel
displayName: Remote devel
@@ -202,14 +228,28 @@ stages:
parameters:
testFormat: devel/{0}/1
targets:
- name: macOS 11.1
test: macos/11.1
- name: macOS 12.0
test: macos/12.0
- name: RHEL 7.9
test: rhel/7.9
- name: RHEL 8.4
test: rhel/8.4
- name: FreeBSD 12.2
test: freebsd/12.2
- name: RHEL 9.0
test: rhel/9.0
- name: FreeBSD 12.3
test: freebsd/12.3
- name: FreeBSD 13.1
test: freebsd/13.1
- stage: Remote_2_13
displayName: Remote 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.13/{0}/1
targets:
- name: macOS 12.0
test: macos/12.0
- name: RHEL 8.5
test: rhel/8.5
- name: FreeBSD 13.0
test: freebsd/13.0
- stage: Remote_2_12
@@ -224,8 +264,6 @@ stages:
test: macos/11.1
- name: RHEL 8.4
test: rhel/8.4
- name: FreeBSD 13.0
test: freebsd/13.0
- stage: Remote_2_11
displayName: Remote 2.11
dependsOn: []
@@ -234,8 +272,6 @@ stages:
parameters:
testFormat: 2.11/{0}/1
targets:
- name: RHEL 7.9
test: rhel/7.9
- name: RHEL 8.3
test: rhel/8.3
- name: FreeBSD 12.2
@@ -252,8 +288,6 @@ stages:
test: osx/10.11
- name: macOS 10.15
test: macos/10.15
- name: FreeBSD 12.1
test: freebsd/12.1
- stage: Remote_2_9
displayName: Remote 2.9
dependsOn: []
@@ -274,7 +308,6 @@ stages:
nameFormat: Python {0}
testFormat: devel/cloud/{0}/1
targets:
- test: 2.6
- test: 2.7
- test: 3.5
- test: 3.6
@@ -282,6 +315,17 @@ stages:
- test: 3.8
- test: 3.9
- test: "3.10"
- stage: Cloud_2_13
displayName: Cloud 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Python {0}
testFormat: 2.13/cloud/{0}/1
targets:
- test: 2.7
- test: 3.7
- stage: Cloud_2_12
displayName: Cloud 2.12
dependsOn: []
@@ -291,6 +335,7 @@ stages:
nameFormat: Python {0}
testFormat: 2.12/cloud/{0}/1
targets:
- test: 2.6
- test: 3.9
- stage: Cloud_2_11
displayName: Cloud 2.11
@@ -321,7 +366,7 @@ stages:
nameFormat: Python {0}
testFormat: 2.9/cloud/{0}/1
targets:
- test: 3.5
- test: 2.7
## Finally
@@ -329,21 +374,26 @@ stages:
condition: succeededOrFailed()
dependsOn:
- Ansible_devel
- Ansible_2_13
- Ansible_2_12
- Ansible_2_11
- Ansible_2_10
- Ansible_2_9
- Remote_devel
- Remote_2_13
- Remote_2_12
- Remote_2_11
- Remote_2_10
- Remote_2_9
- Docker_devel
- Docker_2_13
- Docker_2_12
- Docker_2_11
- Docker_2_10
- Docker_2_9
- Docker_community_devel
- Cloud_devel
- Cloud_2_13
- Cloud_2_12
- Cloud_2_11
- Cloud_2_10

View File

@@ -11,7 +11,7 @@ mkdir "${agent_temp_directory}/coverage/"
options=(--venv --venv-system-site-packages --color -v)
ansible-test coverage combine --export "${agent_temp_directory}/coverage/" "${options[@]}"
ansible-test coverage combine --group-by command --export "${agent_temp_directory}/coverage/" "${options[@]}"
if ansible-test coverage analyze targets generate --help >/dev/null 2>&1; then
# Only analyze coverage if the installed version of ansible-test supports it.

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python
"""
Upload code coverage reports to codecov.io.
Multiple coverage files from multiple languages are accepted and aggregated after upload.
Python coverage, as well as PowerShell and Python stubs can all be uploaded.
"""
import argparse
import dataclasses
import pathlib
import shutil
import subprocess
import tempfile
import typing as t
import urllib.request
@dataclasses.dataclass(frozen=True)
class CoverageFile:
name: str
path: pathlib.Path
flags: t.List[str]
@dataclasses.dataclass(frozen=True)
class Args:
dry_run: bool
path: pathlib.Path
def parse_args() -> Args:
parser = argparse.ArgumentParser()
parser.add_argument('-n', '--dry-run', action='store_true')
parser.add_argument('path', type=pathlib.Path)
args = parser.parse_args()
# Store arguments in a typed dataclass
fields = dataclasses.fields(Args)
kwargs = {field.name: getattr(args, field.name) for field in fields}
return Args(**kwargs)
def process_files(directory: pathlib.Path) -> t.Tuple[CoverageFile, ...]:
processed = []
for file in directory.joinpath('reports').glob('coverage*.xml'):
name = file.stem.replace('coverage=', '')
# Get flags from name
flags = name.replace('-powershell', '').split('=') # Drop '-powershell' suffix
flags = [flag if not flag.startswith('stub') else flag.split('-')[0] for flag in flags] # Remove "-01" from stub files
processed.append(CoverageFile(name, file, flags))
return tuple(processed)
def upload_files(codecov_bin: pathlib.Path, files: t.Tuple[CoverageFile, ...], dry_run: bool = False) -> None:
for file in files:
cmd = [
str(codecov_bin),
'--name', file.name,
'--file', str(file.path),
]
for flag in file.flags:
cmd.extend(['--flags', flag])
if dry_run:
print(f'DRY-RUN: Would run command: {cmd}')
continue
subprocess.run(cmd, check=True)
def download_file(url: str, dest: pathlib.Path, flags: int, dry_run: bool = False) -> None:
if dry_run:
print(f'DRY-RUN: Would download {url} to {dest} and set mode to {flags:o}')
return
with urllib.request.urlopen(url) as resp:
with dest.open('w+b') as f:
# Read data in chunks rather than all at once
shutil.copyfileobj(resp, f, 64 * 1024)
dest.chmod(flags)
def main():
args = parse_args()
url = 'https://ansible-ci-files.s3.amazonaws.com/codecov/linux/codecov'
with tempfile.TemporaryDirectory(prefix='codecov-') as tmpdir:
codecov_bin = pathlib.Path(tmpdir) / 'codecov'
download_file(url, codecov_bin, 0o755, args.dry_run)
files = process_files(args.path)
upload_files(codecov_bin, files, args.dry_run)
if __name__ == '__main__':
main()

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env bash
# Upload code coverage reports to codecov.io.
# Multiple coverage files from multiple languages are accepted and aggregated after upload.
# Python coverage, as well as PowerShell and Python stubs can all be uploaded.
set -o pipefail -eu
output_path="$1"
curl --silent --show-error https://ansible-ci-files.s3.us-east-1.amazonaws.com/codecov/codecov.sh > codecov.sh
for file in "${output_path}"/reports/coverage*.xml; do
name="${file}"
name="${name##*/}" # remove path
name="${name##coverage=}" # remove 'coverage=' prefix if present
name="${name%.xml}" # remove '.xml' suffix
bash codecov.sh \
-f "${file}" \
-n "${name}" \
-X coveragepy \
-X gcov \
-X fix \
-X search \
-X xcode \
|| echo "Failed to upload code coverage report to codecov.io: ${file}"
done

View File

@@ -12,4 +12,4 @@ if ! ansible-test --help >/dev/null 2>&1; then
pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check
fi
ansible-test coverage xml --stub --venv --venv-system-site-packages --color -v
ansible-test coverage xml --group-by command --stub --venv --venv-system-site-packages --color -v

View File

@@ -33,7 +33,7 @@ jobs:
summaryFileLocation: "$(outputPath)/reports/$(pipelinesCoverage).xml"
displayName: Publish to Azure Pipelines
condition: gt(variables.coverageFileCount, 0)
- bash: .azure-pipelines/scripts/publish-codecov.sh "$(outputPath)"
- bash: .azure-pipelines/scripts/publish-codecov.py "$(outputPath)"
displayName: Publish to codecov.io
condition: gt(variables.coverageFileCount, 0)
continueOnError: true

5
.github/patchback.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
---
backport_branch_prefix: patchback/backports/
backport_label_prefix: backport-
target_branch_prefix: stable-
...

109
.github/workflows/ee.yml vendored Normal file
View File

@@ -0,0 +1,109 @@
---
name: execution environment
on:
# Run CI against all pushes (direct commits, also merged PRs), Pull Requests
push:
branches:
- main
- stable-*
pull_request:
# Run CI once per day (at 04:45 UTC)
# This ensures that even if there haven't been commits that we are still testing against latest version of ansible-builder
schedule:
- cron: '45 4 * * *'
env:
NAMESPACE: community
COLLECTION_NAME: crypto
jobs:
build:
name: Build and test EE (Ⓐ${{ matrix.runner_tag }})
strategy:
matrix:
runner_tag:
- devel
- stable-2.12-latest
- stable-2.11-latest
- stable-2.9-latest
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
with:
path: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.10'
- name: Install ansible-builder and ansible-navigator
run: pip install ansible-builder ansible-navigator
- name: Verify requirements
run: ansible-builder introspect --sanitize .
- name: Make sure galaxy.yml has version entry
run: >-
python -c
'import yaml ;
f = open("galaxy.yml", "rb") ;
data = yaml.safe_load(f) ;
f.close() ;
data["version"] = data.get("version") or "0.0.1" ;
f = open("galaxy.yml", "wb") ;
f.write(yaml.dump(data).encode("utf-8")) ;
f.close() ;
'
working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
- name: Build collection
run: |
ansible-galaxy collection build --output-path ../../../
working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
- name: Create files for building execution environment
run: |
COLLECTION_FILENAME="$(ls "${{ env.NAMESPACE }}-${{ env.COLLECTION_NAME }}"-*.tar.gz)"
# EE config
cat > execution-environment.yml <<EOF
---
version: 1
build_arg_defaults:
EE_BASE_IMAGE: 'quay.io/ansible/ansible-runner:${{ matrix.runner_tag }}'
dependencies:
galaxy: requirements.yml
EOF
echo "::group::execution-environment.yml"
cat execution-environment.yml
echo "::endgroup::"
# Requirements
cat > requirements.yml <<EOF
---
collections:
- name: ${COLLECTION_FILENAME}
type: file
EOF
echo "::group::requirements.yml"
cat requirements.yml
echo "::endgroup::"
- name: Build image based on ${{ matrix.runner_tag }}
run: |
mkdir -p context/_build/
cp "${{ env.NAMESPACE }}-${{ env.COLLECTION_NAME }}"-*.tar.gz context/_build/
ansible-builder build -v 3 -t test-ee:latest --container-runtime=podman
- name: Run basic tests
run: >
ansible-navigator run
--mode stdout
--pull-policy never
--set-environment-variable ANSIBLE_PRIVATE_ROLE_VARS=true
--execution-environment-image test-ee:latest
-v
all.yml
working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}/tests/ee

View File

@@ -5,6 +5,268 @@ Community Crypto Release Notes
.. contents:: Topics
v2.3.2
======
Release Summary
---------------
Maintenance and bugfix release.
Bugfixes
--------
- Include ``simplified_bsd.txt`` license file for the ECS module utils.
- certificate_complete_chain - do not stop execution if an unsupported signature algorithm is encountered; warn instead (https://github.com/ansible-collections/community.crypto/pull/457).
v2.3.1
======
Release Summary
---------------
Maintenance release.
Bugfixes
--------
- Include ``PSF-license.txt`` file for ``plugins/module_utils/_version.py``.
v2.3.0
======
Release Summary
---------------
Feature and bugfix release.
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).
- 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).
- 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 ``persistent`` option when opening LUKS2 containers (https://github.com/ansible-collections/community.crypto/pull/434).
- openssl_csr_info - add ``name_encoding`` option to control the encoding (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436).
- openssl_pkcs12 - allow to provide the private key as text instead of having to read it from a file. This allows to store the private key in an encrypted form, for example in Ansible Vault (https://github.com/ansible-collections/community.crypto/pull/452).
- x509_certificate_info - add ``name_encoding`` option to control the encoding (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436).
- x509_crl - add ``name_encoding`` option to control the encoding (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436).
- x509_crl_info - add ``name_encoding`` option to control the encoding (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436).
Bugfixes
--------
- Make collection more robust when PyOpenSSL is used with an incompatible cryptography version (https://github.com/ansible-collections/community.crypto/pull/445).
- x509_crl - fix crash when ``issuer`` for a revoked certificate is specified (https://github.com/ansible-collections/community.crypto/pull/441).
v2.2.4
======
Release Summary
---------------
Regular maintenance release.
Bugfixes
--------
- openssh_* modules - fix exception handling to report traceback to users for enhanced traceability (https://github.com/ansible-collections/community.crypto/pull/417).
v2.2.3
======
Release Summary
---------------
Regular bugfix release.
Bugfixes
--------
- luks_device - fix parsing of ``lsblk`` output when device name ends with ``crypt`` (https://github.com/ansible-collections/community.crypto/issues/409, https://github.com/ansible-collections/community.crypto/pull/410).
v2.2.2
======
Release Summary
---------------
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.
Bugfixes
--------
- certificate_complete_chain - allow multiple potential intermediate certificates to have the same subject (https://github.com/ansible-collections/community.crypto/issues/399, https://github.com/ansible-collections/community.crypto/pull/403).
- x509_certificate - for the ``ownca`` provider, check whether the CA private key actually belongs to the CA certificate (https://github.com/ansible-collections/community.crypto/pull/407).
- x509_certificate - regenerate certificate when the CA's public key changes for ``provider=ownca`` (https://github.com/ansible-collections/community.crypto/pull/407).
- x509_certificate - regenerate certificate when the CA's subject changes for ``provider=ownca`` (https://github.com/ansible-collections/community.crypto/issues/400, https://github.com/ansible-collections/community.crypto/pull/402).
- x509_certificate - regenerate certificate when the private key changes for ``provider=selfsigned`` (https://github.com/ansible-collections/community.crypto/pull/407).
v2.2.1
======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- openssh_cert - fixed false ``changed`` status for ``host`` certificates when using ``full_idempotence`` (https://github.com/ansible-collections/community.crypto/issues/395, https://github.com/ansible-collections/community.crypto/pull/396).
v2.2.0
======
Release Summary
---------------
Regular bugfix and feature release.
Minor Changes
-------------
- openssh_cert - added ``ignore_timestamps`` parameter so it can be used semi-idempotent with relative timestamps in ``valid_to``/``valid_from`` (https://github.com/ansible-collections/community.crypto/issues/379).
Bugfixes
--------
- luks_devices - set ``LANG`` and similar environment variables to avoid translated output, which can break some of the module's functionality like key management (https://github.com/ansible-collections/community.crypto/pull/388, https://github.com/ansible-collections/community.crypto/issues/385).
v2.1.0
======
Release Summary
---------------
Feature and bugfix release.
Minor Changes
-------------
- Adjust error messages that indicate ``cryptography`` is not installed from ``Can't`` to ``Cannot`` (https://github.com/ansible-collections/community.crypto/pull/374).
Bugfixes
--------
- Various modules and plugins - use vendored version of ``distutils.version`` instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.crypto/pull/353).
- certificate_complete_chain - do not append root twice if the chain already ends with a root certificate (https://github.com/ansible-collections/community.crypto/pull/360).
- certificate_complete_chain - do not hang when infinite loop is found (https://github.com/ansible-collections/community.crypto/issues/355, https://github.com/ansible-collections/community.crypto/pull/360).
New Modules
-----------
- crypto_info - Retrieve cryptographic capabilities
- openssl_privatekey_convert - Convert OpenSSL private keys
v2.0.2
======
Release Summary
---------------
Documentation fix release. No actual code changes.
v2.0.1
======
Release Summary
---------------
Bugfix release with extra forward compatibility for newer versions of cryptography.
Minor Changes
-------------
- acme_* modules - fix usage of ``fetch_url`` with changes in latest ansible-core ``devel`` branch (https://github.com/ansible-collections/community.crypto/pull/339).
Bugfixes
--------
- acme_certificate - avoid passing multiple certificates to ``cryptography``'s X.509 certificate loader when ``fullchain_dest`` is used (https://github.com/ansible-collections/community.crypto/pull/324).
- get_certificate, openssl_csr_info, x509_certificate_info - add fallback code for extension parsing that works with cryptography 36.0.0 and newer. This code re-serializes de-serialized extensions and thus can return slightly different values if the extension in the original CSR resp. certificate was not canonicalized correctly. This code is currently used as a fallback if the existing code stops working, but we will switch it to be the main code in a future release (https://github.com/ansible-collections/community.crypto/pull/331).
- luks_device - now also runs a built-in LUKS signature cleaner on ``state=absent`` to make sure that also the secondary LUKS2 header is wiped when older versions of wipefs are used (https://github.com/ansible-collections/community.crypto/issues/326, https://github.com/ansible-collections/community.crypto/pull/327).
- openssl_pkcs12 - use new PKCS#12 deserialization infrastructure from cryptography 36.0.0 if available (https://github.com/ansible-collections/community.crypto/pull/302).
v2.0.0
======
Release Summary
---------------
A new major release of the ``community.crypto`` collection. The main changes are removal of the PyOpenSSL backends for almost all modules (``openssl_pkcs12`` being the only exception), and removal of the ``assertonly`` provider in the ``x509_certificate`` provider. There are also some other breaking changes which should improve the user interface/experience of this collection long-term.
Minor Changes
-------------
- acme_certificate - the ``subject`` and ``issuer`` fields in in the ``select_chain`` entries are now more strictly validated (https://github.com/ansible-collections/community.crypto/pull/316).
- openssl_csr, openssl_csr_pipe - provide a new ``subject_ordered`` option if the order of the components in the subject is of importance (https://github.com/ansible-collections/community.crypto/issues/291, https://github.com/ansible-collections/community.crypto/pull/316).
- openssl_csr, openssl_csr_pipe - there is now stricter validation of the values of the ``subject`` option (https://github.com/ansible-collections/community.crypto/pull/316).
- openssl_privatekey_info - add ``check_consistency`` option to request private key consistency checks to be done (https://github.com/ansible-collections/community.crypto/pull/309).
- x509_certificate, x509_certificate_pipe - add ``ignore_timestamps`` option which allows to enable idempotency for 'not before' and 'not after' options (https://github.com/ansible-collections/community.crypto/issues/295, https://github.com/ansible-collections/community.crypto/pull/317).
- x509_crl - provide a new ``issuer_ordered`` option if the order of the components in the issuer is of importance (https://github.com/ansible-collections/community.crypto/issues/291, https://github.com/ansible-collections/community.crypto/pull/316).
- x509_crl - there is now stricter validation of the values of the ``issuer`` option (https://github.com/ansible-collections/community.crypto/pull/316).
Breaking Changes / Porting Guide
--------------------------------
- Adjust ``dirName`` text parsing and to text converting code to conform to `Sections 2 and 3 of RFC 4514 <https://datatracker.ietf.org/doc/html/rfc4514.html>`_. This is similar to how `cryptography handles this <https://cryptography.io/en/latest/x509/reference/#cryptography.x509.Name.rfc4514_string>`_ (https://github.com/ansible-collections/community.crypto/pull/274).
- acme module utils - removing compatibility code (https://github.com/ansible-collections/community.crypto/pull/290).
- acme_* modules - removed vendored copy of the Python library ``ipaddress``. If you are using Python 2.x, please make sure to install the library (https://github.com/ansible-collections/community.crypto/pull/287).
- compatibility module_utils - removed vendored copy of the Python library ``ipaddress`` (https://github.com/ansible-collections/community.crypto/pull/287).
- crypto module utils - removing compatibility code (https://github.com/ansible-collections/community.crypto/pull/290).
- get_certificate, openssl_csr_info, x509_certificate_info - depending on the ``cryptography`` version used, the modules might not return the ASN.1 value for an extension as contained in the certificate respectively CSR, but a re-encoded version of it. This should usually be identical to the value contained in the source file, unless the value was malformed. For extensions not handled by C(cryptography) the value contained in the source file is always returned unaltered (https://github.com/ansible-collections/community.crypto/pull/318).
- module_utils - removed various PyOpenSSL support functions and default backend values that are not needed for the openssl_pkcs12 module (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_csr, openssl_csr_pipe, x509_crl - the ``subject`` respectively ``issuer`` fields no longer ignore empty values, but instead fail when encountering them (https://github.com/ansible-collections/community.crypto/pull/316).
- openssl_privatekey_info - by default consistency checks are not run; they need to be explicitly requested by passing ``check_consistency=true`` (https://github.com/ansible-collections/community.crypto/pull/309).
- x509_crl - for idempotency checks, the ``issuer`` order is ignored. If order is important, use the new ``issuer_ordered`` option (https://github.com/ansible-collections/community.crypto/pull/316).
Deprecated Features
-------------------
- acme_* modules - ACME version 1 is now deprecated and support for it will be removed in community.crypto 2.0.0 (https://github.com/ansible-collections/community.crypto/pull/288).
Removed Features (previously deprecated)
----------------------------------------
- acme_* modules - the ``acme_directory`` option is now required (https://github.com/ansible-collections/community.crypto/pull/290).
- acme_* modules - the ``acme_version`` option is now required (https://github.com/ansible-collections/community.crypto/pull/290).
- acme_account_facts - the deprecated redirect has been removed. Use community.crypto.acme_account_info instead (https://github.com/ansible-collections/community.crypto/pull/290).
- acme_account_info - ``retrieve_orders=url_list`` no longer returns the return value ``orders``. Use the ``order_uris`` return value instead (https://github.com/ansible-collections/community.crypto/pull/290).
- crypto.info module utils - the deprecated redirect has been removed. Use ``crypto.pem`` instead (https://github.com/ansible-collections/community.crypto/pull/290).
- get_certificate - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_certificate - the deprecated redirect has been removed. Use community.crypto.x509_certificate instead (https://github.com/ansible-collections/community.crypto/pull/290).
- openssl_certificate_info - the deprecated redirect has been removed. Use community.crypto.x509_certificate_info instead (https://github.com/ansible-collections/community.crypto/pull/290).
- openssl_csr - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_csr and openssl_csr_pipe - ``version`` now only accepts the (default) value 1 (https://github.com/ansible-collections/community.crypto/pull/290).
- openssl_csr_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_csr_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_privatekey - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_privatekey_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_privatekey_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_publickey - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_publickey_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_signature - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_signature_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- x509_certificate - remove ``assertonly`` provider (https://github.com/ansible-collections/community.crypto/pull/289).
- x509_certificate - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- x509_certificate_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- x509_certificate_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
Bugfixes
--------
- cryptography backend - improve Unicode handling for Python 2 (https://github.com/ansible-collections/community.crypto/pull/313).
- get_certificate - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
- openssl_pkcs12 - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/296).
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
v1.9.4
======

48
PSF-license.txt Normal file
View File

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

@@ -11,7 +11,7 @@ Please note that this collection does **not** support Windows targets.
## Tested with Ansible
Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11 and ansible-core 2.12 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12 and ansible-core 2.13 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
## External requirements

View File

@@ -562,3 +562,326 @@ releases:
- 279-acme-openssl.yml
- 282-acme_challenge_cert_helper-error.yml
release_date: '2021-09-28'
2.0.0:
changes:
breaking_changes:
- Adjust ``dirName`` text parsing and to text converting code to conform to
`Sections 2 and 3 of RFC 4514 <https://datatracker.ietf.org/doc/html/rfc4514.html>`_.
This is similar to how `cryptography handles this <https://cryptography.io/en/latest/x509/reference/#cryptography.x509.Name.rfc4514_string>`_
(https://github.com/ansible-collections/community.crypto/pull/274).
- acme module utils - removing compatibility code (https://github.com/ansible-collections/community.crypto/pull/290).
- acme_* modules - removed vendored copy of the Python library ``ipaddress``.
If you are using Python 2.x, please make sure to install the library (https://github.com/ansible-collections/community.crypto/pull/287).
- compatibility module_utils - removed vendored copy of the Python library ``ipaddress``
(https://github.com/ansible-collections/community.crypto/pull/287).
- crypto module utils - removing compatibility code (https://github.com/ansible-collections/community.crypto/pull/290).
- get_certificate, openssl_csr_info, x509_certificate_info - depending on the
``cryptography`` version used, the modules might not return the ASN.1 value
for an extension as contained in the certificate respectively CSR, but a re-encoded
version of it. This should usually be identical to the value contained in
the source file, unless the value was malformed. For extensions not handled
by C(cryptography) the value contained in the source file is always returned
unaltered (https://github.com/ansible-collections/community.crypto/pull/318).
- module_utils - removed various PyOpenSSL support functions and default backend
values that are not needed for the openssl_pkcs12 module (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_csr, openssl_csr_pipe, x509_crl - the ``subject`` respectively ``issuer``
fields no longer ignore empty values, but instead fail when encountering them
(https://github.com/ansible-collections/community.crypto/pull/316).
- openssl_privatekey_info - by default consistency checks are not run; they
need to be explicitly requested by passing ``check_consistency=true`` (https://github.com/ansible-collections/community.crypto/pull/309).
- x509_crl - for idempotency checks, the ``issuer`` order is ignored. If order
is important, use the new ``issuer_ordered`` option (https://github.com/ansible-collections/community.crypto/pull/316).
bugfixes:
- cryptography backend - improve Unicode handling for Python 2 (https://github.com/ansible-collections/community.crypto/pull/313).
- get_certificate - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/294).
- openssl_csr_info - fix compatibility with the cryptography 35.0.0 release
(https://github.com/ansible-collections/community.crypto/pull/294).
- openssl_pkcs12 - fix compatibility with the cryptography 35.0.0 release (https://github.com/ansible-collections/community.crypto/pull/296).
- x509_certificate_info - fix compatibility with the cryptography 35.0.0 release
(https://github.com/ansible-collections/community.crypto/pull/294).
deprecated_features:
- acme_* modules - ACME version 1 is now deprecated and support for it will
be removed in community.crypto 2.0.0 (https://github.com/ansible-collections/community.crypto/pull/288).
minor_changes:
- acme_certificate - the ``subject`` and ``issuer`` fields in in the ``select_chain``
entries are now more strictly validated (https://github.com/ansible-collections/community.crypto/pull/316).
- openssl_csr, openssl_csr_pipe - provide a new ``subject_ordered`` option if
the order of the components in the subject is of importance (https://github.com/ansible-collections/community.crypto/issues/291,
https://github.com/ansible-collections/community.crypto/pull/316).
- openssl_csr, openssl_csr_pipe - there is now stricter validation of the values
of the ``subject`` option (https://github.com/ansible-collections/community.crypto/pull/316).
- openssl_privatekey_info - add ``check_consistency`` option to request private
key consistency checks to be done (https://github.com/ansible-collections/community.crypto/pull/309).
- x509_certificate, x509_certificate_pipe - add ``ignore_timestamps`` option
which allows to enable idempotency for 'not before' and 'not after' options
(https://github.com/ansible-collections/community.crypto/issues/295, https://github.com/ansible-collections/community.crypto/pull/317).
- x509_crl - provide a new ``issuer_ordered`` option if the order of the components
in the issuer is of importance (https://github.com/ansible-collections/community.crypto/issues/291,
https://github.com/ansible-collections/community.crypto/pull/316).
- x509_crl - there is now stricter validation of the values of the ``issuer``
option (https://github.com/ansible-collections/community.crypto/pull/316).
release_summary: 'A new major release of the ``community.crypto`` collection.
The main changes are removal of the PyOpenSSL backends for almost all modules
(``openssl_pkcs12`` being the only exception), and removal of the ``assertonly``
provider in the ``x509_certificate`` provider. There are also some other breaking
changes which should improve the user interface/experience of this collection
long-term.
'
removed_features:
- acme_* modules - the ``acme_directory`` option is now required (https://github.com/ansible-collections/community.crypto/pull/290).
- acme_* modules - the ``acme_version`` option is now required (https://github.com/ansible-collections/community.crypto/pull/290).
- acme_account_facts - the deprecated redirect has been removed. Use community.crypto.acme_account_info
instead (https://github.com/ansible-collections/community.crypto/pull/290).
- acme_account_info - ``retrieve_orders=url_list`` no longer returns the return
value ``orders``. Use the ``order_uris`` return value instead (https://github.com/ansible-collections/community.crypto/pull/290).
- crypto.info module utils - the deprecated redirect has been removed. Use ``crypto.pem``
instead (https://github.com/ansible-collections/community.crypto/pull/290).
- get_certificate - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_certificate - the deprecated redirect has been removed. Use community.crypto.x509_certificate
instead (https://github.com/ansible-collections/community.crypto/pull/290).
- openssl_certificate_info - the deprecated redirect has been removed. Use community.crypto.x509_certificate_info
instead (https://github.com/ansible-collections/community.crypto/pull/290).
- openssl_csr - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_csr and openssl_csr_pipe - ``version`` now only accepts the (default)
value 1 (https://github.com/ansible-collections/community.crypto/pull/290).
- openssl_csr_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_csr_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_privatekey - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_privatekey_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_privatekey_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_publickey - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_publickey_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_signature - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- openssl_signature_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- x509_certificate - remove ``assertonly`` provider (https://github.com/ansible-collections/community.crypto/pull/289).
- x509_certificate - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- x509_certificate_info - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
- x509_certificate_pipe - removed the ``pyopenssl`` backend (https://github.com/ansible-collections/community.crypto/pull/273).
fragments:
- 2.0.0.yml
- 273-pyopenssl-removal.yml
- 274-dirname-rfc4514.yml
- 287-remove-ipaddress.yml
- 288-depecate-acme-v1.yml
- 289-assertonly-removed.yml
- 290-remove-deprecations.yml
- 294-cryptography-35.0.0.yml
- 296-openssl_pkcs12-cryptography-35.yml
- 309-openssl_privatekey_info-consistency.yml
- 313-unicode-names.yml
- 315-ordered-names.yml
- 317-ignore-timestamps.yml
- 318-extension-value-note.yml
release_date: '2021-11-01'
2.0.1:
changes:
bugfixes:
- acme_certificate - avoid passing multiple certificates to ``cryptography``'s
X.509 certificate loader when ``fullchain_dest`` is used (https://github.com/ansible-collections/community.crypto/pull/324).
- get_certificate, openssl_csr_info, x509_certificate_info - add fallback code
for extension parsing that works with cryptography 36.0.0 and newer. This
code re-serializes de-serialized extensions and thus can return slightly different
values if the extension in the original CSR resp. certificate was not canonicalized
correctly. This code is currently used as a fallback if the existing code
stops working, but we will switch it to be the main code in a future release
(https://github.com/ansible-collections/community.crypto/pull/331).
- luks_device - now also runs a built-in LUKS signature cleaner on ``state=absent``
to make sure that also the secondary LUKS2 header is wiped when older versions
of wipefs are used (https://github.com/ansible-collections/community.crypto/issues/326,
https://github.com/ansible-collections/community.crypto/pull/327).
- openssl_pkcs12 - use new PKCS#12 deserialization infrastructure from cryptography
36.0.0 if available (https://github.com/ansible-collections/community.crypto/pull/302).
minor_changes:
- acme_* modules - fix usage of ``fetch_url`` with changes in latest ansible-core
``devel`` branch (https://github.com/ansible-collections/community.crypto/pull/339).
release_summary: Bugfix release with extra forward compatibility for newer versions
of cryptography.
fragments:
- 2.0.1.yml
- 302-openssl_pkcs12-cryptography-36.0.0.yml
- 324-acme_certificate-fullchain.yml
- 327-luks_device-wipe.yml
- 331-cryptography-extensions.yml
- fetch_url-devel.yml
release_date: '2021-11-22'
2.0.2:
changes:
release_summary: Documentation fix release. No actual code changes.
fragments:
- 2.0.2.yml
release_date: '2021-12-20'
2.1.0:
changes:
bugfixes:
- Various modules and plugins - use vendored version of ``distutils.version``
instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.crypto/pull/353).
- certificate_complete_chain - do not append root twice if the chain already
ends with a root certificate (https://github.com/ansible-collections/community.crypto/pull/360).
- certificate_complete_chain - do not hang when infinite loop is found (https://github.com/ansible-collections/community.crypto/issues/355,
https://github.com/ansible-collections/community.crypto/pull/360).
minor_changes:
- Adjust error messages that indicate ``cryptography`` is not installed from
``Can't`` to ``Cannot`` (https://github.com/ansible-collections/community.crypto/pull/374).
release_summary: Feature and bugfix release.
fragments:
- 2.1.0.yml
- 353-distutils.version.yml
- 360-certificate_complete_chain-loop.yml
- 374-docs.yml
modules:
- description: Retrieve cryptographic capabilities
name: crypto_info
namespace: ''
- description: Convert OpenSSL private keys
name: openssl_privatekey_convert
namespace: ''
release_date: '2022-01-10'
2.2.0:
changes:
bugfixes:
- luks_devices - set ``LANG`` and similar environment variables to avoid translated
output, which can break some of the module's functionality like key management
(https://github.com/ansible-collections/community.crypto/pull/388, https://github.com/ansible-collections/community.crypto/issues/385).
minor_changes:
- openssh_cert - added ``ignore_timestamps`` parameter so it can be used semi-idempotent
with relative timestamps in ``valid_to``/``valid_from`` (https://github.com/ansible-collections/community.crypto/issues/379).
release_summary: Regular bugfix and feature release.
fragments:
- 2.2.0.yml
- 381_openssh_cert_add_ignore_timestamps.yml
- 388-luks_device-i18n.yml
release_date: '2022-02-01'
2.2.1:
changes:
bugfixes:
- openssh_cert - fixed false ``changed`` status for ``host`` certificates when
using ``full_idempotence`` (https://github.com/ansible-collections/community.crypto/issues/395,
https://github.com/ansible-collections/community.crypto/pull/396).
release_summary: Bugfix release.
fragments:
- 2.2.1.yml
- 396-openssh_cert-host-cert-idempotence-fix.yml
release_date: '2022-02-05'
2.2.2:
changes:
bugfixes:
- certificate_complete_chain - allow multiple potential intermediate certificates
to have the same subject (https://github.com/ansible-collections/community.crypto/issues/399,
https://github.com/ansible-collections/community.crypto/pull/403).
- x509_certificate - for the ``ownca`` provider, check whether the CA private
key actually belongs to the CA certificate (https://github.com/ansible-collections/community.crypto/pull/407).
- x509_certificate - regenerate certificate when the CA's public key changes
for ``provider=ownca`` (https://github.com/ansible-collections/community.crypto/pull/407).
- x509_certificate - regenerate certificate when the CA's subject changes for
``provider=ownca`` (https://github.com/ansible-collections/community.crypto/issues/400,
https://github.com/ansible-collections/community.crypto/pull/402).
- x509_certificate - regenerate certificate when the private key changes for
``provider=selfsigned`` (https://github.com/ansible-collections/community.crypto/pull/407).
release_summary: '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.
'
fragments:
- 2.2.2.yml
- 402-x509_certificate-ownca-subject.yml
- 403-certificate_complete_chain-same-subject.yml
- 407-x509_certificate-signature.yml
release_date: '2022-02-21'
2.2.3:
changes:
bugfixes:
- luks_device - fix parsing of ``lsblk`` output when device name ends with ``crypt``
(https://github.com/ansible-collections/community.crypto/issues/409, https://github.com/ansible-collections/community.crypto/pull/410).
release_summary: Regular bugfix release.
fragments:
- 2.2.3.yml
- 410-luks_device-lsblk-parsing.yml
release_date: '2022-03-04'
2.2.4:
changes:
bugfixes:
- openssh_* modules - fix exception handling to report traceback to users for
enhanced traceability (https://github.com/ansible-collections/community.crypto/pull/417).
release_summary: Regular maintenance release.
fragments:
- 2.2.4.yml
- 417-openssh_modules-fix-exception-reporting.yml
release_date: '2022-03-22'
2.3.0:
changes:
bugfixes:
- Make collection more robust when PyOpenSSL is used with an incompatible cryptography
version (https://github.com/ansible-collections/community.crypto/pull/445).
- x509_crl - fix crash when ``issuer`` for a revoked certificate is specified
(https://github.com/ansible-collections/community.crypto/pull/441).
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).
- 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).
- 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 ``persistent`` option when opening LUKS2 containers (https://github.com/ansible-collections/community.crypto/pull/434).
- openssl_csr_info - add ``name_encoding`` option to control the encoding (IDNA,
Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436).
- openssl_pkcs12 - allow to provide the private key as text instead of having
to read it from a file. This allows to store the private key in an encrypted
form, for example in Ansible Vault (https://github.com/ansible-collections/community.crypto/pull/452).
- x509_certificate_info - add ``name_encoding`` option to control the encoding
(IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436).
- x509_crl - add ``name_encoding`` option to control the encoding (IDNA, Unicode)
used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436).
- x509_crl_info - add ``name_encoding`` option to control the encoding (IDNA,
Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436).
release_summary: Feature and bugfix release.
fragments:
- 2.3.0.yml
- 434-add-persistent-and-perf-options.yml
- 436-idns.yml
- 440-ee.yml
- 441-x509-crl-cert-issuer.yml
- 445-fix.yml
- 448-acme-request-timeouts.yml
- 452-openssl_pkcs12-private-key-content.yml
release_date: '2022-05-09'
2.3.1:
changes:
bugfixes:
- Include ``PSF-license.txt`` file for ``plugins/module_utils/_version.py``.
release_summary: Maintenance release.
fragments:
- 2.3.1.yml
- psf-license.yml
release_date: '2022-05-16'
2.3.2:
changes:
bugfixes:
- Include ``simplified_bsd.txt`` license file for the ECS module utils.
- certificate_complete_chain - do not stop execution if an unsupported signature
algorithm is encountered; warn instead (https://github.com/ansible-collections/community.crypto/pull/457).
release_summary: Maintenance and bugfix release.
fragments:
- 2.3.2.yml
- 457-certificate_complete_chain-unsupported-algorithm.yml
- simplified-bsd-license.yml
release_date: '2022-06-02'

23
docs/docsite/links.yml Normal file
View File

@@ -0,0 +1,23 @@
---
edit_on_github:
repository: ansible-collections/community.crypto
branch: main
path_prefix: ''
extra_links:
- description: Submit a bug report
url: https://github.com/ansible-collections/community.crypto/issues/new?assignees=&labels=&template=bug_report.md
- description: Request a feature
url: https://github.com/ansible-collections/community.crypto/issues/new?assignees=&labels=&template=feature_request.md
communication:
matrix_rooms:
- topic: General usage and support questions
room: '#users:ansible.im'
irc_channels:
- topic: General usage and support questions
network: Libera
channel: '#ansible'
mailing_lists:
- topic: Ansible Project List
url: https://groups.google.com/g/ansible-project

View File

@@ -71,7 +71,7 @@ In the following example, we assume that the certificate to sign (including its
- name: Sign certificate with our CA
community.crypto.x509_certificate_pipe:
csr_content: "{{ ca_csr.csr }}"
csr_content: "{{ csr.csr }}"
provider: ownca
ownca_path: /path/to/ca-certificate.pem
ownca_privatekey_path: /path/to/ca-certificate.key
@@ -128,7 +128,7 @@ Please note that the above procedure is **not idempotent**. The following extend
- name: Sign certificate with our CA
community.crypto.x509_certificate_pipe:
content: "{{ (certificate.content | b64decode) if certificate_exists.stat.exists else omit }}"
csr_content: "{{ ca_csr.csr }}"
csr_content: "{{ csr.csr }}"
provider: ownca
ownca_path: /path/to/ca-certificate.pem
ownca_privatekey_path: /path/to/ca-certificate.key

View File

@@ -1,6 +1,6 @@
namespace: community
name: crypto
version: 1.9.4
version: 2.3.2
readme: README.md
authors:
- Ansible (github.com/ansible)

10
meta/ee-bindep.txt Normal file
View File

@@ -0,0 +1,10 @@
cryptsetup [platform:dpkg]
cryptsetup [platform:rpm]
openssh-client [platform:dpkg]
openssh-clients [platform:rpm]
openssl [platform:dpkg]
openssl [platform:rpm]
python3-cryptography [platform:dpkg]
python3-cryptography [platform:rpm]
python3-openssl [platform:dpkg]
python3-pyOpenSSL [platform:rpm]

1
meta/ee-requirements.txt Normal file
View File

@@ -0,0 +1 @@
PyYAML

View File

@@ -0,0 +1,5 @@
---
version: 1
dependencies:
python: meta/ee-requirements.txt
system: meta/ee-bindep.txt

View File

@@ -12,20 +12,19 @@ action_groups:
plugin_routing:
modules:
acme_account_facts:
deprecation:
tombstone:
removal_version: 2.0.0
warning_text: The 'community.crypto.acme_account_facts' module has been renamed to 'community.crypto.acme_account_info'.
openssl_certificate:
deprecation:
tombstone:
removal_version: 2.0.0
warning_text: The 'community.crypto.openssl_certificate' module has been renamed to 'community.crypto.x509_certificate'
openssl_certificate_info:
deprecation:
tombstone:
removal_version: 2.0.0
warning_text: The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_certificate_info'
module_utils:
crypto.identify:
redirect: community.crypto.crypto.pem
deprecation:
tombstone:
removal_version: 2.0.0
warning_text: The 'crypto/identify.py' module_utils has been renamed 'crypto/pem.py'. Please update your imports

View File

@@ -23,9 +23,17 @@ notes:
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:
- python >= 2.6
- either openssl or L(cryptography,https://cryptography.io/) >= 1.5
- ipaddress
options:
account_key_src:
description:
@@ -33,7 +41,7 @@ options:
key."
- "Private keys can be created with the
M(community.crypto.openssl_privatekey) or M(community.crypto.openssl_privatekey_pipe)
modules. If the requisites (pyOpenSSL or cryptography) are not available,
modules. If the requisite (cryptography) is not available,
keys can also be created directly with the C(openssl) command line tool:
RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys
can be created with C(openssl ecparam -genkey ...). Any other tool creating
@@ -74,9 +82,9 @@ options:
- "The ACME version of the endpoint."
- "Must be C(1) for the classic Let's Encrypt and Buypass ACME endpoints,
or C(2) for standardized ACME v2 endpoints."
- "The default value is C(1). Note that in community.crypto 2.0.0, this
option B(will be required) and will no longer have a default."
- "Please also note that we will deprecate ACME v1 support eventually."
- "The value C(1) is deprecated since community.crypto 2.0.0 and will be
removed from community.crypto 3.0.0."
required: true
type: int
choices: [ 1, 2 ]
acme_directory:
@@ -86,33 +94,19 @@ options:
- "For safety reasons the default is set to the Let's Encrypt staging
server (for the ACME v1 protocol). This will create technically correct,
but untrusted certificates."
- "The default value is C(https://acme-staging.api.letsencrypt.org/directory).
Note that in community.crypto 2.0.0, this option B(will be required) and
will no longer have a default. Note that the default is the Let's Encrypt
staging server for the ACME v1 protocol, which is deprecated and will
be disabled in May 2021 (see
L(here,https://community.letsencrypt.org/t/end-of-life-plan-for-acmev1/88430/7)
for details)."
- "For Let's Encrypt, all staging endpoints can be found here:
U(https://letsencrypt.org/docs/staging-environment/). For Buypass, all
endpoints can be found here:
U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)"
- "For B(Let's Encrypt), the production directory URL for ACME v2 is
U(https://acme-v02.api.letsencrypt.org/directory).
(The production directory URL for ACME v1 is
U(https://acme-v01.api.letsencrypt.org/directory) and will be
disabled in July 2021.)"
U(https://acme-v02.api.letsencrypt.org/directory)."
- "For B(Buypass), the production directory URL for ACME v2 and v1 is
U(https://api.buypass.com/acme/directory)."
- "For B(ZeroSSL), the production directory URL for ACME v2 is
U(https://acme.zerossl.com/v2/DV90)."
- "B(Warning:) So far, the ACME modules have only been tested against Let's Encrypt
(staging and production), Buypass (staging and production), ZeroSSL (production),
and L(Pebble testing server,https://github.com/letsencrypt/Pebble). 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."
- The notes for this module contain a list of ACME services this module has
been tested against.
required: true
type: str
validate_certs:
description:
@@ -132,4 +126,11 @@ options:
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

@@ -14,12 +14,9 @@ class ModuleDocFragment(object):
DOCUMENTATION = r'''
description:
- This module allows one to (re)generate OpenSSL certificates.
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL.
- If both the cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with C(select_crypto_backend)).
Please note that the PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0.
- It uses the cryptography python library to interact with OpenSSL.
requirements:
- PyOpenSSL >= 0.15 or cryptography >= 1.6 (if using C(selfsigned), C(ownca) or C(assertonly) provider)
- cryptography >= 1.6 (if using C(selfsigned) or C(ownca) provider)
options:
force:
description:
@@ -55,17 +52,22 @@ options:
- This is required if the private key is password protected.
type: str
ignore_timestamps:
description:
- Whether the "not before" and "not after" timestamps should be ignored for idempotency checks.
- It is better to keep the default value C(true) when using relative timestamps (like C(+0s) for now).
type: bool
default: true
version_added: 2.0.0
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
From that point on, only the C(cryptography) backend will be available.
type: str
default: auto
choices: [ auto, cryptography, pyopenssl ]
choices: [ auto, cryptography ]
notes:
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
@@ -119,201 +121,6 @@ options:
default: https://acme-v02.api.letsencrypt.org/directory
'''
BACKEND_ASSERTONLY_DOCUMENTATION = r'''
description:
- The C(assertonly) provider is intended for use cases where one is only interested in
checking properties of a supplied certificate. Please note that this provider has been
deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0. See the examples on how
to emulate C(assertonly) usage with M(community.crypto.x509_certificate_info),
M(community.crypto.openssl_csr_info), M(community.crypto.openssl_privatekey_info) and
M(ansible.builtin.assert). This also allows more flexible checks than
the ones offered by the C(assertonly) provider.
- Many properties that can be specified in this module are for validation of an
existing or newly generated certificate. The proper place to specify them, if you
want to receive a certificate with these properties is a CSR (Certificate Signing Request).
options:
csr_path:
description:
- This is not required for the C(assertonly) provider.
csr_content:
description:
- This is not required for the C(assertonly) provider.
signature_algorithms:
description:
- A list of algorithms that you would accept the certificate to be signed with
(e.g. ['sha256WithRSAEncryption', 'sha512WithRSAEncryption']).
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: list
elements: str
issuer:
description:
- The key/value pairs that must be present in the issuer name field of the certificate.
- If you need to specify more than one value with the same key, use a list as value.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: dict
issuer_strict:
description:
- If set to C(yes), the I(issuer) field must contain only these values.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: bool
default: no
subject:
description:
- The key/value pairs that must be present in the subject name field of the certificate.
- If you need to specify more than one value with the same key, use a list as value.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: dict
subject_strict:
description:
- If set to C(yes), the I(subject) field must contain only these values.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: bool
default: no
has_expired:
description:
- Checks if the certificate is expired/not expired at the time the module is executed.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: bool
default: no
version:
description:
- The version of the certificate.
- Nowadays it should almost always be 3.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: int
valid_at:
description:
- The certificate must be valid at this point in time.
- The timestamp is formatted as an ASN.1 TIME.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: str
invalid_at:
description:
- The certificate must be invalid at this point in time.
- The timestamp is formatted as an ASN.1 TIME.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: str
not_before:
description:
- The certificate must start to become valid at this point in time.
- The timestamp is formatted as an ASN.1 TIME.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: str
aliases: [ notBefore ]
not_after:
description:
- The certificate must expire at this point in time.
- The timestamp is formatted as an ASN.1 TIME.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: str
aliases: [ notAfter ]
valid_in:
description:
- The certificate must still be valid at this relative time offset from now.
- Valid format is C([+-]timespec | number_of_seconds) where timespec can be an integer
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
- Note that if using this parameter, this module is NOT idempotent.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: str
key_usage:
description:
- The I(key_usage) extension field must contain all these values.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: list
elements: str
aliases: [ keyUsage ]
key_usage_strict:
description:
- If set to C(yes), the I(key_usage) extension field must contain only these values.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: bool
default: no
aliases: [ keyUsage_strict ]
extended_key_usage:
description:
- The I(extended_key_usage) extension field must contain all these values.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: list
elements: str
aliases: [ extendedKeyUsage ]
extended_key_usage_strict:
description:
- If set to C(yes), the I(extended_key_usage) extension field must contain only these values.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: bool
default: no
aliases: [ extendedKeyUsage_strict ]
subject_alt_name:
description:
- The I(subject_alt_name) extension field must contain these values.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: list
elements: str
aliases: [ subjectAltName ]
subject_alt_name_strict:
description:
- If set to C(yes), the I(subject_alt_name) extension field must contain only these values.
- This is only used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: bool
default: no
aliases: [ subjectAltName_strict ]
'''
BACKEND_ENTRUST_DOCUMENTATION = r'''
options:
entrust_cert_type:
@@ -386,6 +193,7 @@ options:
- The minimum certificate lifetime is 90 days, and maximum is three years.
- If this value is not specified, the certificate will stop being valid 365 days the date of issue.
- This is only used by the C(entrust) provider.
- Please note that this value is B(not) covered by the I(ignore_timestamps) option.
type: str
default: +365d
@@ -456,9 +264,11 @@ options:
- 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]) (e.g. C(+32w1d2h).
- Note that if using relative time this module is NOT idempotent.
+ 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
@@ -469,9 +279,11 @@ options:
- 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]) (e.g. C(+32w1d2h).
- Note that if using relative time this module is NOT idempotent.
+ 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.
@@ -547,9 +359,11 @@ options:
- 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]) (e.g. C(+32w1d2h).
- Note that if using relative time this module is NOT idempotent.
+ 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
@@ -561,9 +375,11 @@ options:
- 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]) (e.g. C(+32w1d2h).
- Note that if using relative time this module is NOT idempotent.
+ 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.

View File

@@ -15,13 +15,8 @@ description:
- This module allows one to (re)generate OpenSSL certificate signing requests.
- This module supports the subjectAltName, keyUsage, extendedKeyUsage, basicConstraints and OCSP Must Staple
extensions.
- "The module can use the cryptography Python library, or the pyOpenSSL Python
library. By default, it tries to detect which one is available. This can be
overridden with the I(select_crypto_backend) option. Please note that the
PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
requirements:
- Either cryptography >= 1.3
- Or pyOpenSSL >= 0.15
- cryptography >= 1.3
options:
digest:
description:
@@ -48,14 +43,29 @@ options:
- The version of the certificate signing request.
- "The only allowed value according to L(RFC 2986,https://tools.ietf.org/html/rfc2986#section-4.1)
is 1."
- This option will no longer accept unsupported values from community.crypto 2.0.0 on.
- This option no longer accepts unsupported values since community.crypto 2.0.0.
type: int
default: 1
choices:
- 1
subject:
description:
- Key/value pairs that will be present in the subject name field of the certificate signing request.
- If you need to specify more than one value with the same key, use a list as value.
- If the order of the components is important, use I(subject_ordered).
- Mutually exclusive with I(subject_ordered).
type: dict
subject_ordered:
description:
- A list of dictionaries, where every dictionary must contain one key/value pair. This key/value pair
will be present in the subject name field of the certificate signing request.
- If you want to specify more than one value with the same key in a row, you can use a list as value.
- Mutually exclusive with I(subject), and any other subject field option, such as I(country_name),
I(state_or_province_name), I(locality_name), I(organization_name), I(organizational_unit_name),
I(common_name), or I(email_address).
type: list
elements: dict
version_added: 2.0.0
country_name:
description:
- The countryName field of the certificate signing request subject.
@@ -94,9 +104,8 @@ options:
subject_alt_name:
description:
- Subject Alternative Name (SAN) extension to attach to the certificate signing request.
- This can either be a 'comma separated string' or a YAML list.
- 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).
- 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).
@@ -119,7 +128,7 @@ options:
aliases: [ useCommonNameForSAN ]
key_usage:
description:
- This defines the purpose (e.g. encipherment, signature, certificate signing)
- This defines the purpose (for example encipherment, signature, certificate signing)
of the key contained in the certificate.
type: list
elements: str
@@ -132,7 +141,7 @@ options:
aliases: [ keyUsage_critical ]
extended_key_usage:
description:
- Additional restrictions (e.g. client authentication, server authentication)
- Additional restrictions (for example client authentication, server authentication)
on the allowed purposes for which the public key may be used.
type: list
elements: str
@@ -196,14 +205,11 @@ options:
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
From that point on, only the C(cryptography) backend will be available.
type: str
default: auto
choices: [ auto, cryptography, pyopenssl ]
choices: [ auto, cryptography ]
create_subject_key_identifier:
description:
- Create the Subject Key Identifier from the public key.

View File

@@ -17,18 +17,13 @@ description:
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 don't match
- "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."
- "The module can use the cryptography Python library, or the pyOpenSSL Python
library. By default, it tries to detect which one is available. This can be
overridden with the I(select_crypto_backend) option. Please note that the
PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
requirements:
- Either cryptography >= 1.2.3 (older versions might work as well)
- Or pyOpenSSL
- cryptography >= 1.2.3 (older versions might work as well)
options:
size:
description:
@@ -80,33 +75,25 @@ options:
type: str
cipher:
description:
- The cipher to encrypt the private key. (Valid values can be found by
running `openssl list -cipher-algorithms` or `openssl list-cipher-algorithms`,
depending on your OpenSSL version.)
- When using the C(cryptography) backend, use C(auto).
- The cipher to encrypt the private key. Must be C(auto).
type: str
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
From that point on, only the C(cryptography) backend will be available.
type: str
default: auto
choices: [ auto, cryptography, pyopenssl ]
choices: [ auto, cryptography ]
format:
description:
- Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format)
is used for all keys which support it. Please note that not every key can be exported in any format.
- The value C(auto) selects a fromat based on the key format. The value C(auto_ignore) does the same,
- The value C(auto) selects a format based on the key format. The value C(auto_ignore) does the same,
but for existing private key files, it will not force a regenerate when its format is not the automatically
selected one for generation.
- Note that if the format for an existing private key mismatches, the key is B(regenerated) by default.
To change this behavior, use the I(format_mismatch) option.
- The I(format) option is only supported by the C(cryptography) backend. The C(pyopenssl) backend will
fail if a value different from C(auto_ignore) is used.
type: str
default: auto_ignore
choices: [ pkcs1, pkcs8, raw, auto, auto_ignore ]
@@ -124,12 +111,12 @@ options:
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 doesn't match the module's options,
- 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
isn't matching, and will never regenerate an existing key.
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

View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2022, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
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

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2022, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
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,343 @@
# Vendored copy of distutils/version.py from CPython 3.9.5
#
# Implements multiple version numbering conventions for the
# Python Module Distribution Utilities.
#
# PSF License (see PSF-license.txt or https://opensource.org/licenses/Python-2.0)
#
"""Provides classes to represent module version numbers (one class for
each style of version numbering). There are currently two such classes
implemented: StrictVersion and LooseVersion.
Every version number class implements the following interface:
* the 'parse' method takes a string and parses it to some internal
representation; if the string is an invalid version number,
'parse' raises a ValueError exception
* the class constructor takes an optional string argument which,
if supplied, is passed to 'parse'
* __str__ reconstructs the string that was passed to 'parse' (or
an equivalent string -- ie. one that will generate an equivalent
version number instance)
* __repr__ generates Python code to recreate the version number instance
* _cmp compares the current instance with either another instance
of the same class or a string (which will be parsed to an instance
of the same class, thus must follow the same rules)
"""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
try:
RE_FLAGS = re.VERBOSE | re.ASCII
except AttributeError:
RE_FLAGS = re.VERBOSE
class Version:
"""Abstract base class for version numbering classes. Just provides
constructor (__init__) and reproducer (__repr__), because those
seem to be the same for all version numbering classes; and route
rich comparisons to _cmp.
"""
def __init__(self, vstring=None):
if vstring:
self.parse(vstring)
def __repr__(self):
return "%s ('%s')" % (self.__class__.__name__, str(self))
def __eq__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c == 0
def __lt__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c < 0
def __le__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c <= 0
def __gt__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c > 0
def __ge__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c >= 0
# Interface for version-number classes -- must be implemented
# by the following classes (the concrete ones -- Version should
# be treated as an abstract class).
# __init__ (string) - create and take same action as 'parse'
# (string parameter is optional)
# parse (string) - convert a string representation to whatever
# internal representation is appropriate for
# this style of version numbering
# __str__ (self) - convert back to a string; should be very similar
# (if not identical to) the string supplied to parse
# __repr__ (self) - generate Python code to recreate
# the instance
# _cmp (self, other) - compare two version numbers ('other' may
# be an unparsed version string, or another
# instance of your version class)
class StrictVersion(Version):
"""Version numbering for anal retentives and software idealists.
Implements the standard interface for version number classes as
described above. A version number consists of two or three
dot-separated numeric components, with an optional "pre-release" tag
on the end. The pre-release tag consists of the letter 'a' or 'b'
followed by a number. If the numeric components of two version
numbers are equal, then one with a pre-release tag will always
be deemed earlier (lesser) than one without.
The following are valid version numbers (shown in the order that
would be obtained by sorting according to the supplied cmp function):
0.4 0.4.0 (these two are equivalent)
0.4.1
0.5a1
0.5b3
0.5
0.9.6
1.0
1.0.4a3
1.0.4b1
1.0.4
The following are examples of invalid version numbers:
1
2.7.2.2
1.3.a4
1.3pl1
1.3c4
The rationale for this version numbering system will be explained
in the distutils documentation.
"""
version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$',
RE_FLAGS)
def parse(self, vstring):
match = self.version_re.match(vstring)
if not match:
raise ValueError("invalid version number '%s'" % vstring)
(major, minor, patch, prerelease, prerelease_num) = \
match.group(1, 2, 4, 5, 6)
if patch:
self.version = tuple(map(int, [major, minor, patch]))
else:
self.version = tuple(map(int, [major, minor])) + (0,)
if prerelease:
self.prerelease = (prerelease[0], int(prerelease_num))
else:
self.prerelease = None
def __str__(self):
if self.version[2] == 0:
vstring = '.'.join(map(str, self.version[0:2]))
else:
vstring = '.'.join(map(str, self.version))
if self.prerelease:
vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
return vstring
def _cmp(self, other):
if isinstance(other, str):
other = StrictVersion(other)
elif not isinstance(other, StrictVersion):
return NotImplemented
if self.version != other.version:
# numeric versions don't match
# prerelease stuff doesn't matter
if self.version < other.version:
return -1
else:
return 1
# have to compare prerelease
# case 1: neither has prerelease; they're equal
# case 2: self has prerelease, other doesn't; other is greater
# case 3: self doesn't have prerelease, other does: self is greater
# case 4: both have prerelease: must compare them!
if (not self.prerelease and not other.prerelease):
return 0
elif (self.prerelease and not other.prerelease):
return -1
elif (not self.prerelease and other.prerelease):
return 1
elif (self.prerelease and other.prerelease):
if self.prerelease == other.prerelease:
return 0
elif self.prerelease < other.prerelease:
return -1
else:
return 1
else:
raise AssertionError("never get here")
# end class StrictVersion
# The rules according to Greg Stein:
# 1) a version number has 1 or more numbers separated by a period or by
# sequences of letters. If only periods, then these are compared
# left-to-right to determine an ordering.
# 2) sequences of letters are part of the tuple for comparison and are
# compared lexicographically
# 3) recognize the numeric components may have leading zeroes
#
# The LooseVersion class below implements these rules: a version number
# string is split up into a tuple of integer and string components, and
# comparison is a simple tuple comparison. This means that version
# numbers behave in a predictable and obvious way, but a way that might
# not necessarily be how people *want* version numbers to behave. There
# wouldn't be a problem if people could stick to purely numeric version
# numbers: just split on period and compare the numbers as tuples.
# However, people insist on putting letters into their version numbers;
# the most common purpose seems to be:
# - indicating a "pre-release" version
# ('alpha', 'beta', 'a', 'b', 'pre', 'p')
# - indicating a post-release patch ('p', 'pl', 'patch')
# but of course this can't cover all version number schemes, and there's
# no way to know what a programmer means without asking him.
#
# The problem is what to do with letters (and other non-numeric
# characters) in a version number. The current implementation does the
# obvious and predictable thing: keep them as strings and compare
# lexically within a tuple comparison. This has the desired effect if
# an appended letter sequence implies something "post-release":
# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
#
# However, if letters in a version number imply a pre-release version,
# the "obvious" thing isn't correct. Eg. you would expect that
# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
# implemented here, this just isn't so.
#
# Two possible solutions come to mind. The first is to tie the
# comparison algorithm to a particular set of semantic rules, as has
# been done in the StrictVersion class above. This works great as long
# as everyone can go along with bondage and discipline. Hopefully a
# (large) subset of Python module programmers will agree that the
# particular flavour of bondage and discipline provided by StrictVersion
# provides enough benefit to be worth using, and will submit their
# version numbering scheme to its domination. The free-thinking
# anarchists in the lot will never give in, though, and something needs
# to be done to accommodate them.
#
# Perhaps a "moderately strict" version class could be implemented that
# lets almost anything slide (syntactically), and makes some heuristic
# assumptions about non-digits in version number strings. This could
# sink into special-case-hell, though; if I was as talented and
# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
# just as happy dealing with things like "2g6" and "1.13++". I don't
# think I'm smart enough to do it right though.
#
# In any case, I've coded the test suite for this module (see
# ../test/test_version.py) specifically to fail on things like comparing
# "1.2a2" and "1.2". That's not because the *code* is doing anything
# wrong, it's because the simple, obvious design doesn't match my
# complicated, hairy expectations for real-world version numbers. It
# would be a snap to fix the test suite to say, "Yep, LooseVersion does
# the Right Thing" (ie. the code matches the conception). But I'd rather
# have a conception that matches common notions about version numbers.
class LooseVersion(Version):
"""Version numbering for anarchists and software realists.
Implements the standard interface for version number classes as
described above. A version number consists of a series of numbers,
separated by either periods or strings of letters. When comparing
version numbers, the numeric components will be compared
numerically, and the alphabetic components lexically. The following
are all valid version numbers, in no particular order:
1.5.1
1.5.2b2
161
3.10a
8.02
3.4j
1996.07.12
3.2.pl0
3.1.1.6
2g6
11g
0.960923
2.2beta29
1.13++
5.5.kw
2.0b1pl0
In fact, there is no such thing as an invalid version number under
this scheme; the rules for comparison are simple and predictable,
but may not always give the results you want (for some definition
of "want").
"""
component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
def __init__(self, vstring=None):
if vstring:
self.parse(vstring)
def parse(self, vstring):
# I've given up on thinking I can reconstruct the version string
# from the parsed tuple -- so I just store the string here for
# use by __str__
self.vstring = vstring
components = [x for x in self.component_re.split(vstring) if x and x != '.']
for i, obj in enumerate(components):
try:
components[i] = int(obj)
except ValueError:
pass
self.version = components
def __str__(self):
return self.vstring
def __repr__(self):
return "LooseVersion ('%s')" % str(self)
def _cmp(self, other):
if isinstance(other, str):
other = LooseVersion(other)
elif not isinstance(other, LooseVersion):
return NotImplemented
if self.version == other.version:
return 0
if self.version < other.version:
return -1
if self.version > other.version:
return 1
# end class LooseVersion

View File

@@ -1,90 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import base64
import binascii
import copy
import datetime
import hashlib
import json
import locale
import os
import re
import shutil
import sys
import tempfile
import traceback
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.six.moves.urllib.parse import unquote
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
get_default_argspec,
ACMEDirectory,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import (
CryptographyBackend,
CRYPTOGRAPHY_VERSION,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
OpenSSLCLIBackend,
)
from ansible_collections.community.crypto.plugins.module_utils.acme._compatibility import (
handle_standard_module_arguments,
set_crypto_backend,
HAS_CURRENT_CRYPTOGRAPHY,
)
from ansible_collections.community.crypto.plugins.module_utils.acme._compatibility import ACMELegacyAccount as ACMEAccount
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
from ansible_collections.community.crypto.plugins.module_utils.acme.io import (
read_file,
write_file,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
nopad_b64,
pem_to_der,
process_links,
)
def openssl_get_csr_identifiers(openssl_binary, module, csr_filename, csr_content=None):
module.deprecate(
'Please adjust your custom module/plugin to the ACME module_utils refactor '
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
'your code', version='2.0.0', collection_name='community.crypto')
return OpenSSLCLIBackend(module, openssl_binary=openssl_binary).get_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content)
def cryptography_get_csr_identifiers(module, csr_filename, csr_content=None):
module.deprecate(
'Please adjust your custom module/plugin to the ACME module_utils refactor '
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
'your code', version='2.0.0', collection_name='community.crypto')
return CryptographyBackend(module).get_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content)
def cryptography_get_cert_days(module, cert_file, now=None):
module.deprecate(
'Please adjust your custom module/plugin to the ACME module_utils refactor '
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
'your code', version='2.0.0', collection_name='community.crypto')
return CryptographyBackend(module).get_cert_days(cert_filename=cert_file, now=now)

View File

@@ -1,267 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# Copyright: (c) 2021 Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import locale
from ansible.module_utils.basic import missing_required_lib
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import HAS_CURRENT_CRYPTOGRAPHY as _ORIGINAL_HAS_CURRENT_CRYPTOGRAPHY
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import (
CryptographyBackend,
CRYPTOGRAPHY_VERSION,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
OpenSSLCLIBackend,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
ACMEClient,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
ACMEAccount,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import (
create_key_authorization,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
KeyParsingError,
)
HAS_CURRENT_CRYPTOGRAPHY = _ORIGINAL_HAS_CURRENT_CRYPTOGRAPHY
def set_crypto_backend(module):
'''
Sets which crypto backend to use (default: auto detection).
Does not care whether a new enough cryptoraphy is available or not. Must
be called before any real stuff is done which might evaluate
``HAS_CURRENT_CRYPTOGRAPHY``.
'''
global HAS_CURRENT_CRYPTOGRAPHY
module.deprecate(
'Please adjust your custom module/plugin to the ACME module_utils refactor '
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
'your code', version='2.0.0', collection_name='community.crypto')
# Choose backend
backend = module.params['select_crypto_backend']
if backend == 'auto':
pass
elif backend == 'openssl':
HAS_CURRENT_CRYPTOGRAPHY = False
elif backend == 'cryptography':
if not _ORIGINAL_HAS_CURRENT_CRYPTOGRAPHY:
module.fail_json(msg=missing_required_lib('cryptography'))
HAS_CURRENT_CRYPTOGRAPHY = True
else:
module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend))
# Inform about choices
if HAS_CURRENT_CRYPTOGRAPHY:
module.debug('Using cryptography backend (library version {0})'.format(CRYPTOGRAPHY_VERSION))
return 'cryptography'
else:
module.debug('Using OpenSSL binary backend')
return 'openssl'
def handle_standard_module_arguments(module, needs_acme_v2=False):
'''
Do standard module setup, argument handling and warning emitting.
'''
backend = set_crypto_backend(module)
if not module.params['validate_certs']:
module.warn(
'Disabling certificate validation for communications with ACME endpoint. '
'This should only be done for testing against a local ACME server for '
'development purposes, but *never* for production purposes.'
)
if module.params['acme_version'] is None:
module.params['acme_version'] = 1
module.deprecate("The option 'acme_version' will be required from community.crypto 2.0.0 on",
version='2.0.0', collection_name='community.crypto')
if module.params['acme_directory'] is None:
module.params['acme_directory'] = 'https://acme-staging.api.letsencrypt.org/directory'
module.deprecate("The option 'acme_directory' will be required from community.crypto 2.0.0 on",
version='2.0.0', collection_name='community.crypto')
if needs_acme_v2 and module.params['acme_version'] < 2:
module.fail_json(msg='The {0} module requires the ACME v2 protocol!'.format(module._name))
# AnsibleModule() changes the locale, so change it back to C because we rely on time.strptime() when parsing certificate dates.
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
locale.setlocale(locale.LC_ALL, 'C')
return backend
def get_compatibility_backend(module):
if HAS_CURRENT_CRYPTOGRAPHY:
return CryptographyBackend(module)
else:
return OpenSSLCLIBackend(module)
class ACMELegacyAccount(object):
'''
ACME account object. Handles the authorized communication with the
ACME server. Provides access to account bound information like
the currently active authorizations and valid certificates
'''
def __init__(self, module):
module.deprecate(
'Please adjust your custom module/plugin to the ACME module_utils refactor '
'(https://github.com/ansible-collections/community.crypto/pull/184). The '
'compatibility layer will be removed in community.crypto 2.0.0, thus breaking '
'your code', version='2.0.0', collection_name='community.crypto')
backend = get_compatibility_backend(module)
self.client = ACMEClient(module, backend)
self.account = ACMEAccount(self.client)
self.key = self.client.account_key_file
self.key_content = self.client.account_key_content
self.uri = self.client.account_uri
self.key_data = self.client.account_key_data
self.jwk = self.client.account_jwk
self.jws_header = self.client.account_jws_header
self.directory = self.client.directory
def get_keyauthorization(self, token):
'''
Returns the key authorization for the given token
https://tools.ietf.org/html/rfc8555#section-8.1
'''
return create_key_authorization(self.client, token)
def parse_key(self, key_file=None, key_content=None):
'''
Parses an RSA or Elliptic Curve key file in PEM format and returns a pair
(error, key_data).
'''
try:
return None, self.client.parse_key(key_file=key_file, key_content=key_content)
except KeyParsingError as e:
return e.msg, {}
def sign_request(self, protected, payload, key_data, encode_payload=True):
return self.client.sign_request(protected, payload, key_data, encode_payload=encode_payload)
def send_signed_request(self, url, payload, key_data=None, jws_header=None, parse_json_result=True, encode_payload=True):
'''
Sends a JWS signed HTTP POST request to the ACME server and returns
the response as dictionary
https://tools.ietf.org/html/rfc8555#section-6.2
If payload is None, a POST-as-GET is performed.
(https://tools.ietf.org/html/rfc8555#section-6.3)
'''
return self.client.send_signed_request(
url,
payload,
key_data=key_data,
jws_header=jws_header,
parse_json_result=parse_json_result,
encode_payload=encode_payload,
fail_on_error=False,
)
def get_request(self, uri, parse_json_result=True, headers=None, get_only=False, fail_on_error=True):
'''
Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback
to GET if server replies with a status code of 405.
'''
return self.client.get_request(
uri,
parse_json_result=parse_json_result,
headers=headers,
get_only=get_only,
fail_on_error=fail_on_error,
)
def set_account_uri(self, uri):
'''
Set account URI. For ACME v2, it needs to be used to sending signed
requests.
'''
self.client.set_account_uri(uri)
self.uri = self.client.account_uri
def get_account_data(self):
'''
Retrieve account information. Can only be called when the account
URI is already known (such as after calling setup_account).
Return None if the account was deactivated, or a dict otherwise.
'''
return self.account.get_account_data()
def setup_account(self, contact=None, agreement=None, terms_agreed=False,
allow_creation=True, remove_account_uri_if_not_exists=False,
external_account_binding=None):
'''
Detect or create an account on the ACME server. For ACME v1,
as the only way (without knowing an account URI) to test if an
account exists is to try and create one with the provided account
key, this method will always result in an account being present
(except on error situations). For ACME v2, a new account will
only be created if ``allow_creation`` is set to True.
For ACME v2, ``check_mode`` is fully respected. For ACME v1, the
account might be created if it does not yet exist.
Return a pair ``(created, account_data)``. Here, ``created`` will
be ``True`` in case the account was created or would be created
(check mode). ``account_data`` will be the current account data,
or ``None`` if the account does not exist.
The account URI will be stored in ``self.uri``; if it is ``None``,
the account does not exist.
If specified, ``external_account_binding`` should be a dictionary
with keys ``kid``, ``alg`` and ``key``
(https://tools.ietf.org/html/rfc8555#section-7.3.4).
https://tools.ietf.org/html/rfc8555#section-7.3
'''
result = self.account.setup_account(
contact=contact,
agreement=agreement,
terms_agreed=terms_agreed,
allow_creation=allow_creation,
remove_account_uri_if_not_exists=remove_account_uri_if_not_exists,
external_account_binding=external_account_binding,
)
self.uri = self.client.account_uri
return result
def update_account(self, account_data, contact=None):
'''
Update an account on the ACME server. Check mode is fully respected.
The current account data must be provided as ``account_data``.
Return a pair ``(updated, account_data)``, where ``updated`` is
``True`` in case something changed (contact info updated) or
would be changed (check mode), and ``account_data`` the updated
account data.
https://tools.ietf.org/html/rfc8555#section-7.3.2
'''
return self.account.update_account(account_data, contact=contact)

View File

@@ -118,10 +118,10 @@ class ACMEAccount(object):
self.client.set_account_uri(info['location'])
return False, result
elif info['status'] == 400 and result['type'] == 'urn:ietf:params:acme:error:accountDoesNotExist' and not allow_creation:
# Account does not exist (and we didn't try to create it)
# Account does not exist (and we did not try to create it)
return False, None
elif info['status'] == 403 and result['type'] == 'urn:ietf:params:acme:error:unauthorized' and 'deactivated' in (result.get('detail') or ''):
# Account has been deactivated; currently works for Pebble; hasn't been
# 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:

View File

@@ -12,10 +12,12 @@ import copy
import datetime
import json
import locale
import traceback
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.common.text.converters import to_bytes
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.six import PY3
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
OpenSSLCLIBackend,
@@ -38,6 +40,14 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.utils import
nopad_b64,
)
try:
import ipaddress
except ImportError:
HAS_IPADDRESS = False
IPADDRESS_IMPORT_ERROR = traceback.format_exc()
else:
HAS_IPADDRESS = True
def _assert_fetch_url_success(module, response, info, allow_redirect=False, allow_client_error=True, allow_server_error=True):
if info['status'] < 0:
@@ -73,6 +83,8 @@ class ACMEDirectory(object):
self.directory, dummy = account.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 == 1:
for key in ('new-reg', 'new-authz', 'new-cert'):
@@ -93,7 +105,7 @@ class ACMEDirectory(object):
url = self.directory_root if self.version == 1 else self.directory['newNonce']
if resource is not None:
url = resource
dummy, info = fetch_url(self.module, url, method='HEAD')
dummy, info = fetch_url(self.module, url, method='HEAD', timeout=self.request_timeout)
if info['status'] not in (200, 204):
raise NetworkException("Failed to get replay-nonce, got status {0}".format(info['status']))
return info['replay-nonce']
@@ -121,6 +133,8 @@ class ACMEClient(object):
# 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
@@ -225,12 +239,17 @@ class ACMEClient(object):
headers = {
'Content-Type': 'application/jose+json',
}
resp, info = fetch_url(self.module, url, data=data, headers=headers, method='POST')
resp, info = fetch_url(self.module, url, data=data, headers=headers, method='POST', timeout=self.request_timeout)
_assert_fetch_url_success(self.module, resp, info)
result = {}
try:
# In Python 2, reading from a closed response yields a TypeError.
# In Python 3, read() simply returns ''
if PY3 and resp.closed:
raise TypeError
content = resp.read()
except AttributeError:
except (AttributeError, TypeError):
content = info.pop('body', None)
if content or not parse_json_result:
@@ -279,13 +298,17 @@ class ACMEClient(object):
if get_only:
# Perform unauthenticated GET
resp, info = fetch_url(self.module, uri, method='GET', headers=headers)
resp, info = fetch_url(self.module, uri, method='GET', headers=headers, timeout=self.request_timeout)
_assert_fetch_url_success(self.module, resp, info)
try:
# In Python 2, reading from a closed response yields a TypeError.
# In Python 3, read() simply returns ''
if PY3 and resp.closed:
raise TypeError
content = resp.read()
except AttributeError:
except (AttributeError, TypeError):
content = info.pop('body', None)
# Process result
@@ -319,14 +342,18 @@ def get_default_argspec():
account_key_content=dict(type='str', no_log=True),
account_key_passphrase=dict(type='str', no_log=True),
account_uri=dict(type='str'),
acme_directory=dict(type='str'),
acme_version=dict(type='int', choices=[1, 2]),
acme_directory=dict(type='str', required=True),
acme_version=dict(type='int', required=True, choices=[1, 2]),
validate_certs=dict(type='bool', default=True),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']),
request_timeout=dict(type='int', default=10),
)
def create_backend(module, needs_acme_v2):
if not HAS_IPADDRESS:
module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMPORT_ERROR)
backend = module.params['select_crypto_backend']
# Backend autodetect
@@ -353,19 +380,13 @@ def create_backend(module, needs_acme_v2):
'development purposes, but *never* for production purposes.'
)
if module.params['acme_version'] is None:
module.params['acme_version'] = 1
module.deprecate("The option 'acme_version' will be required from community.crypto 2.0.0 on",
version='2.0.0', collection_name='community.crypto')
if module.params['acme_directory'] is None:
module.params['acme_directory'] = 'https://acme-staging.api.letsencrypt.org/directory'
module.deprecate("The option 'acme_directory' will be required from community.crypto 2.0.0 on",
version='2.0.0', collection_name='community.crypto')
if needs_acme_v2 and module.params['acme_version'] < 2:
module.fail_json(msg='The {0} module requires the ACME v2 protocol!'.format(module._name))
if module.params['acme_version'] == 1:
module.deprecate("The value 1 for 'acme_version' is deprecated. Please switch to ACME v2",
version='3.0.0', collection_name='community.crypto')
# AnsibleModule() changes the locale, so change it back to C because we rely
# on datetime.datetime.strptime() when parsing certificate dates.
locale.setlocale(locale.LC_ALL, 'C')

View File

@@ -14,7 +14,9 @@ import datetime
import os
import sys
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
CryptoBackend,
@@ -41,6 +43,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
cryptography_name_to_oid,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
extract_first_pem,
)
try:
import cryptography
import cryptography.hazmat.backends
@@ -53,7 +59,6 @@ try:
import cryptography.hazmat.primitives.serialization
import cryptography.x509
import cryptography.x509.oid
from distutils.version import LooseVersion
CRYPTOGRAPHY_VERSION = cryptography.__version__
HAS_CURRENT_CRYPTOGRAPHY = (LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion('1.5'))
if HAS_CURRENT_CRYPTOGRAPHY:
@@ -118,11 +123,11 @@ class CryptographyChainMatcher(ChainMatcher):
self.issuer = []
if criterium.subject:
self.subject = [
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.subject)
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.subject, 'subject')
]
if criterium.issuer:
self.issuer = [
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.issuer)
(cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.issuer, 'issuer')
]
self.subject_key_identifier = CryptographyChainMatcher._parse_key_identifier(
criterium.subject_key_identifier, 'subject_key_identifier', criterium.index, module)
@@ -187,7 +192,7 @@ class CryptographyBackend(CryptoBackend):
Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
Raises KeyParsingError in case of errors.
'''
# If key_content isn't given, read key_file
# If key_content is not given, read key_file
if key_content is None:
key_content = read_file(key_file)
else:
@@ -357,6 +362,9 @@ class CryptographyBackend(CryptoBackend):
if cert_content is None:
return -1
# Make sure we have at most one PEM. Otherwise cryptography 36.0.0 will barf.
cert_content = to_bytes(extract_first_pem(to_text(cert_content)) or '')
try:
cert = cryptography.x509.load_pem_x509_certificate(cert_content, _cryptography_backend)
except Exception as e:

View File

@@ -29,7 +29,10 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
try:
import ipaddress
except ImportError:
pass
_OPENSSL_ENVIRONMENT_UPDATE = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
@@ -49,7 +52,7 @@ class OpenSSLCLIBackend(CryptoBackend):
'''
if passphrase is not None:
raise KeyParsingError('openssl backend does not support key passphrases')
# If key_file isn't given, but key_content, write that to a temporary file
# If key_file is not given, but key_content, write that to a temporary file
if key_file is None:
fd, tmpsrc = tempfile.mkstemp()
self.module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
@@ -216,9 +219,9 @@ class OpenSSLCLIBackend(CryptoBackend):
@staticmethod
def _normalize_ip(ip):
try:
return to_native(compat_ipaddress.ip_address(to_text(ip)).compressed)
return to_native(ipaddress.ip_address(to_text(ip)).compressed)
except ValueError:
# We don't want to error out on something IPAddress() can't parse
# We do not want to error out on something IPAddress() cannot parse
return ip
def get_csr_identifiers(self, csr_filename=None, csr_content=None):

View File

@@ -16,8 +16,6 @@ import time
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
nopad_b64,
)
@@ -28,6 +26,11 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
ModuleFailException,
)
try:
import ipaddress
except ImportError:
pass
def create_key_authorization(client, token):
'''
@@ -110,7 +113,7 @@ class Challenge(object):
# https://www.rfc-editor.org/rfc/rfc8737.html#section-3
if identifier_type == 'ip':
# IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596)
resource = compat_ipaddress.ip_address(identifier).reverse_pointer
resource = ipaddress.ip_address(identifier).reverse_pointer
if not resource.endswith('.'):
resource += '.'
else:

View File

@@ -7,8 +7,8 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.six import binary_type
from ansible.module_utils.common.text.converters import to_text
from ansible.module_utils.six import binary_type, PY3
def format_error_problem(problem, subproblem_prefix=''):
@@ -52,8 +52,12 @@ class ACMEProtocolException(ModuleFailException):
# Try to get hold of content, if response is given and content is not provided
if content is None and content_json is None and response is not None:
try:
# In Python 2, reading from a closed response yields a TypeError.
# In Python 3, read() simply returns ''
if PY3 and response.closed:
raise TypeError
content = response.read()
except AttributeError:
except (AttributeError, TypeError):
content = info.pop('body', None)
# Make sure that content_json is None or a dictionary

File diff suppressed because it is too large Load Diff

View File

@@ -1,99 +0,0 @@
# -*- coding: utf-8 -*-
#
# (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
# THIS FILE IS FOR COMPATIBILITY ONLY! YOU SHALL NOT IMPORT IT!
#
# This fill will be removed eventually, so if you're using it,
# please stop doing so.
from .basic import (
HAS_PYOPENSSL,
CRYPTOGRAPHY_HAS_X25519,
CRYPTOGRAPHY_HAS_X25519_FULL,
CRYPTOGRAPHY_HAS_X448,
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_ED448,
HAS_CRYPTOGRAPHY,
OpenSSLObjectError,
OpenSSLBadPassphraseError,
)
from .cryptography_crl import (
REVOCATION_REASON_MAP,
REVOCATION_REASON_MAP_INVERSE,
cryptography_decode_revoked_certificate,
)
from .cryptography_support import (
cryptography_get_extensions_from_cert,
cryptography_get_extensions_from_csr,
cryptography_name_to_oid,
cryptography_oid_to_name,
cryptography_get_name,
cryptography_decode_name,
cryptography_parse_key_usage_params,
cryptography_get_basic_constraints,
cryptography_key_needs_digest_for_signing,
cryptography_compare_public_keys,
)
from .pem import (
identify_private_key_format,
)
from .math import (
binary_exp_mod,
simple_gcd,
quick_is_not_prime,
count_bits,
)
from ._obj2txt import obj2txt as _obj2txt
from ._objects_data import OID_MAP as _OID_MAP
from ._objects import OID_LOOKUP as _OID_LOOKUP
from ._objects import NORMALIZE_NAMES as _NORMALIZE_NAMES
from ._objects import NORMALIZE_NAMES_SHORT as _NORMALIZE_NAMES_SHORT
from .pyopenssl_support import (
pyopenssl_normalize_name,
pyopenssl_get_extensions_from_cert,
pyopenssl_get_extensions_from_csr,
)
from .support import (
get_fingerprint_of_bytes,
get_fingerprint,
load_privatekey,
load_certificate,
load_certificate_request,
parse_name_field,
convert_relative_to_datetime,
get_relative_time_option,
select_message_digest,
OpenSSLObject,
)
from ..io import (
load_file_if_exists,
write_file,
)

View File

@@ -20,6 +20,10 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
# WARNING: this function no longer works with cryptography 35.0.0 and newer!
# It must **ONLY** be used in compatibility code for older
# cryptography versions!
def obj2txt(openssl_lib, openssl_ffi, obj):
# Set to 80 on the recommendation of
# https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values

View File

@@ -20,15 +20,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
from distutils.version import LooseVersion
try:
import OpenSSL # noqa
from OpenSSL import crypto # noqa
HAS_PYOPENSSL = True
except ImportError:
# Error handled in the calling module.
HAS_PYOPENSSL = False
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
try:
import cryptography

View File

@@ -95,12 +95,12 @@ def cryptography_decode_revoked_certificate(cert):
return result
def cryptography_dump_revoked(entry):
def cryptography_dump_revoked(entry, idn_rewrite='ignore'):
return {
'serial_number': entry['serial_number'],
'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT),
'issuer':
[cryptography_decode_name(issuer) for issuer in entry['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,
@@ -116,7 +116,7 @@ def cryptography_get_signature_algorithm_oid_from_crl(crl):
try:
return crl.signature_algorithm_oid
except AttributeError:
# Older cryptography versions don't have signature_algorithm_oid yet
# Older cryptography versions do not have signature_algorithm_oid yet
dotted = obj2txt(
crl._backend._lib,
crl._backend._ffi,

View File

@@ -22,19 +22,49 @@ __metaclass__ = type
import base64
import binascii
import re
import sys
import traceback
from ansible.module_utils.common.text.converters import to_text, to_bytes
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlunparse, ParseResult
from ._asn1 import serialize_asn1_string_as_der
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
try:
import cryptography
from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
import ipaddress
except ImportError:
# Error handled in the calling module.
pass
try:
import cryptography.hazmat.primitives.asymmetric.rsa
except ImportError:
pass
try:
import cryptography.hazmat.primitives.asymmetric.ec
except ImportError:
pass
try:
import cryptography.hazmat.primitives.asymmetric.dsa
except ImportError:
pass
try:
import cryptography.hazmat.primitives.asymmetric.ed25519
except ImportError:
pass
try:
import cryptography.hazmat.primitives.asymmetric.ed448
except ImportError:
pass
try:
# This is a separate try/except since this is only present in cryptography 2.5 or newer
from cryptography.hazmat.primitives.serialization.pkcs12 import (
@@ -44,9 +74,36 @@ except ImportError:
# Error handled in the calling module.
_load_key_and_certificates = None
try:
# This is a separate try/except since this is only present in cryptography 36.0.0 or newer
from cryptography.hazmat.primitives.serialization.pkcs12 import (
load_pkcs12 as _load_pkcs12,
)
except ImportError:
# Error handled in the calling module.
_load_pkcs12 = None
try:
import idna
HAS_IDNA = True
except ImportError:
HAS_IDNA = False
IDNA_IMP_ERROR = traceback.format_exc()
from ansible.module_utils.basic import missing_required_lib
from .basic import (
CRYPTOGRAPHY_HAS_DSA_SIGN,
CRYPTOGRAPHY_HAS_EC_SIGN,
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_ED25519_SIGN,
CRYPTOGRAPHY_HAS_ED448,
CRYPTOGRAPHY_HAS_ED448_SIGN,
CRYPTOGRAPHY_HAS_RSA_SIGN,
CRYPTOGRAPHY_HAS_X25519,
CRYPTOGRAPHY_HAS_X25519_FULL,
CRYPTOGRAPHY_HAS_X448,
OpenSSLObjectError,
)
@@ -64,60 +121,114 @@ DOTTED_OID = re.compile(r'^\d+(?:\.\d+)+$')
def cryptography_get_extensions_from_cert(cert):
# Since cryptography won't give us the DER value for an extension
# (that is only stored for unrecognized extensions), we have to re-do
# the extension parsing outselves.
result = dict()
backend = cert._backend
x509_obj = cert._x509
try:
# Since cryptography will not give us the DER value for an extension
# (that is only stored for unrecognized extensions), we have to re-do
# the extension parsing outselves.
backend = default_backend()
try:
# For certain old versions of cryptography, backend is a MultiBackend object,
# which has no _lib attribute. In that case, revert to the old approach.
backend._lib
except AttributeError:
backend = cert._backend
x509_obj = cert._x509
# With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does
# not allow to get the raw value of an extension, so we have to use this ugly hack:
exts = list(cert.extensions)
for i in range(backend._lib.X509_get_ext_count(x509_obj)):
ext = backend._lib.X509_get_ext(x509_obj, i)
if ext == backend._ffi.NULL:
continue
crit = backend._lib.X509_EXTENSION_get_critical(ext)
data = backend._lib.X509_EXTENSION_get_data(ext)
backend.openssl_assert(data != backend._ffi.NULL)
der = backend._ffi.buffer(data.data, data.length)[:]
entry = dict(
critical=(crit == 1),
value=base64.b64encode(der),
)
try:
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
except AttributeError:
oid = exts[i].oid.dotted_string
result[oid] = entry
except Exception:
# In case the above method breaks, we likely have cryptography 36.0.0 or newer.
# Use it's public_bytes() feature in that case. We will later switch this around
# so that this code will be the default, but for now this will act as a fallback
# since it will re-serialize de-serialized data, which can be different (if the
# original data was not canonicalized) from what was contained in the certificate.
for ext in cert.extensions:
result[ext.oid.dotted_string] = dict(
critical=ext.critical,
value=base64.b64encode(ext.value.public_bytes()),
)
for i in range(backend._lib.X509_get_ext_count(x509_obj)):
ext = backend._lib.X509_get_ext(x509_obj, i)
if ext == backend._ffi.NULL:
continue
crit = backend._lib.X509_EXTENSION_get_critical(ext)
data = backend._lib.X509_EXTENSION_get_data(ext)
backend.openssl_assert(data != backend._ffi.NULL)
der = backend._ffi.buffer(data.data, data.length)[:]
entry = dict(
critical=(crit == 1),
value=base64.b64encode(der),
)
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
result[oid] = entry
return result
def cryptography_get_extensions_from_csr(csr):
# Since cryptography won't give us the DER value for an extension
# (that is only stored for unrecognized extensions), we have to re-do
# the extension parsing outselves.
result = dict()
backend = csr._backend
try:
# Since cryptography will not give us the DER value for an extension
# (that is only stored for unrecognized extensions), we have to re-do
# the extension parsing outselves.
backend = default_backend()
try:
# For certain old versions of cryptography, backend is a MultiBackend object,
# which has no _lib attribute. In that case, revert to the old approach.
backend._lib
except AttributeError:
backend = csr._backend
extensions = backend._lib.X509_REQ_get_extensions(csr._x509_req)
extensions = backend._ffi.gc(
extensions,
lambda ext: backend._lib.sk_X509_EXTENSION_pop_free(
ext,
backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free")
extensions = backend._lib.X509_REQ_get_extensions(csr._x509_req)
extensions = backend._ffi.gc(
extensions,
lambda ext: backend._lib.sk_X509_EXTENSION_pop_free(
ext,
backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free")
)
)
)
for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)):
ext = backend._lib.sk_X509_EXTENSION_value(extensions, i)
if ext == backend._ffi.NULL:
continue
crit = backend._lib.X509_EXTENSION_get_critical(ext)
data = backend._lib.X509_EXTENSION_get_data(ext)
backend.openssl_assert(data != backend._ffi.NULL)
der = backend._ffi.buffer(data.data, data.length)[:]
entry = dict(
critical=(crit == 1),
value=base64.b64encode(der),
)
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
result[oid] = entry
# With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does
# not allow to get the raw value of an extension, so we have to use this ugly hack:
exts = list(csr.extensions)
for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)):
ext = backend._lib.sk_X509_EXTENSION_value(extensions, i)
if ext == backend._ffi.NULL:
continue
crit = backend._lib.X509_EXTENSION_get_critical(ext)
data = backend._lib.X509_EXTENSION_get_data(ext)
backend.openssl_assert(data != backend._ffi.NULL)
der = backend._ffi.buffer(data.data, data.length)[:]
entry = dict(
critical=(crit == 1),
value=base64.b64encode(der),
)
try:
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
except AttributeError:
oid = exts[i].oid.dotted_string
result[oid] = entry
except Exception:
# In case the above method breaks, we likely have cryptography 36.0.0 or newer.
# Use it's public_bytes() feature in that case. We will later switch this around
# so that this code will be the default, but for now this will act as a fallback
# since it will re-serialize de-serialized data, which can be different (if the
# original data was not canonicalized) from what was contained in the CSR.
for ext in csr.extensions:
result[ext.oid.dotted_string] = dict(
critical=ext.critical,
value=base64.b64encode(ext.value.public_bytes()),
)
return result
@@ -161,34 +272,68 @@ def _parse_hex(bytesstr):
return data
DN_COMPONENT_START_RE = re.compile(r'^ *([a-zA-z0-9]+) *= *')
DN_COMPONENT_START_RE = re.compile(b'^ *([a-zA-z0-9.]+) *= *')
DN_HEX_LETTER = b'0123456789abcdef'
def _parse_dn_component(name, sep=',', sep_str='\\', decode_remainder=True):
if sys.version_info[0] < 3:
_int_to_byte = chr
else:
def _int_to_byte(value):
return bytes((value, ))
def _parse_dn_component(name, sep=b',', decode_remainder=True):
m = DN_COMPONENT_START_RE.match(name)
if not m:
raise OpenSSLObjectError('cannot start part in "{0}"'.format(name))
oid = cryptography_name_to_oid(m.group(1))
raise OpenSSLObjectError(u'cannot start part in "{0}"'.format(to_text(name)))
oid = cryptography_name_to_oid(to_text(m.group(1)))
idx = len(m.group(0))
decoded_name = []
sep_str = sep + b'\\'
if decode_remainder:
length = len(name)
while idx < length:
i = idx
while i < length and name[i] not in sep_str:
i += 1
if i > idx:
decoded_name.append(name[idx:i])
idx = i
while idx + 1 < length and name[idx] == '\\':
decoded_name.append(name[idx + 1])
if length > idx and name[idx:idx + 1] == b'#':
# Decoding a hex string
idx += 1
while idx + 1 < length:
ch1 = name[idx:idx + 1]
ch2 = name[idx + 1:idx + 2]
idx1 = DN_HEX_LETTER.find(ch1.lower())
idx2 = DN_HEX_LETTER.find(ch2.lower())
if idx1 < 0 or idx2 < 0:
raise OpenSSLObjectError(u'Invalid hex sequence entry "{0}"'.format(to_text(ch1 + ch2)))
idx += 2
if idx < length and name[idx] == sep:
break
decoded_name.append(_int_to_byte(idx1 * 16 + idx2))
else:
# Decoding a regular string
while idx < length:
i = idx
while i < length and name[i:i + 1] not in sep_str:
i += 1
if i > idx:
decoded_name.append(name[idx:i])
idx = i
while idx + 1 < length and name[idx:idx + 1] == b'\\':
ch = name[idx + 1:idx + 2]
idx1 = DN_HEX_LETTER.find(ch.lower())
if idx1 >= 0:
if idx + 2 >= length:
raise OpenSSLObjectError(u'Hex escape sequence "\\{0}" incomplete at end of string'.format(to_text(ch)))
ch2 = name[idx + 2:idx + 3]
idx2 = DN_HEX_LETTER.find(ch2.lower())
if idx2 < 0:
raise OpenSSLObjectError(u'Hex escape sequence "\\{0}" has invalid second letter'.format(to_text(ch + ch2)))
ch = _int_to_byte(idx1 * 16 + idx2)
idx += 1
idx += 2
decoded_name.append(ch)
if idx < length and name[idx:idx + 1] == sep:
break
else:
decoded_name.append(name[idx:])
idx = len(name)
return x509.NameAttribute(oid, ''.join(decoded_name)), name[idx:]
return x509.NameAttribute(oid, to_text(b''.join(decoded_name))), name[idx:]
def _parse_dn(name):
@@ -199,21 +344,20 @@ def _parse_dn(name):
'''
original_name = name
name = name.lstrip()
sep = ','
if name.startswith('/'):
sep = '/'
sep = b','
if name.startswith(b'/'):
sep = b'/'
name = name[1:]
sep_str = sep + '\\'
result = []
while name:
try:
attribute, name = _parse_dn_component(name, sep=sep, sep_str=sep_str)
attribute, name = _parse_dn_component(name, sep=sep)
except OpenSSLObjectError as e:
raise OpenSSLObjectError('Error while parsing distinguished name "{0}": {1}'.format(original_name, e))
raise OpenSSLObjectError(u'Error while parsing distinguished name "{0}": {1}'.format(to_text(original_name), e))
result.append(attribute)
if name:
if name[0] != sep or len(name) < 2:
raise OpenSSLObjectError('Error while parsing distinguished name "{0}": unexpected end of string'.format(original_name))
if name[0:1] != sep or len(name) < 2:
raise OpenSSLObjectError(u'Error while parsing distinguished name "{0}": unexpected end of string'.format(to_text(original_name)))
name = name[1:]
return result
@@ -222,12 +366,86 @@ def cryptography_parse_relative_distinguished_name(rdn):
names = []
for part in rdn:
try:
names.append(_parse_dn_component(to_text(part), decode_remainder=False)[0])
names.append(_parse_dn_component(to_bytes(part), decode_remainder=False)[0])
except OpenSSLObjectError as e:
raise OpenSSLObjectError('Error while parsing relative distinguished name "{0}": {1}'.format(part, e))
raise OpenSSLObjectError(u'Error while parsing relative distinguished name "{0}": {1}'.format(part, e))
return cryptography.x509.RelativeDistinguishedName(names)
def _is_ascii(value):
'''Check whether the Unicode string `value` contains only ASCII characters.'''
try:
value.encode("ascii")
return True
except UnicodeEncodeError:
return False
def _adjust_idn(value, idn_rewrite):
if idn_rewrite == 'ignore' or not value:
return value
if idn_rewrite == 'idna' and _is_ascii(value):
return value
if idn_rewrite not in ('idna', 'unicode'):
raise ValueError('Invalid value for idn_rewrite: "{0}"'.format(idn_rewrite))
if not HAS_IDNA:
raise OpenSSLObjectError(
missing_required_lib('idna', reason='to transform {what} DNS name "{name}" to {dest}'.format(
name=value,
what='IDNA' if idn_rewrite == 'unicode' else 'Unicode',
dest='Unicode' if idn_rewrite == 'unicode' else 'IDNA',
)))
# Since IDNA does not like '*' or empty labels (except one empty label at the end),
# we split and let IDNA only handle labels that are neither empty or '*'.
parts = value.split(u'.')
for index, part in enumerate(parts):
if part in (u'', u'*'):
continue
try:
if idn_rewrite == 'idna':
parts[index] = idna.encode(part).decode('ascii')
elif idn_rewrite == 'unicode' and part.startswith(u'xn--'):
parts[index] = idna.decode(part)
except idna.IDNAError as exc2008:
try:
if idn_rewrite == 'idna':
parts[index] = part.encode('idna').decode('ascii')
elif idn_rewrite == 'unicode' and part.startswith(u'xn--'):
parts[index] = part.encode('ascii').decode('idna')
except Exception as exc2003:
raise OpenSSLObjectError(
u'Error while transforming part "{part}" of {what} DNS name "{name}" to {dest}.'
u' IDNA2008 transformation resulted in "{exc2008}", IDNA2003 transformation resulted in "{exc2003}".'.format(
part=part,
name=value,
what='IDNA' if idn_rewrite == 'unicode' else 'Unicode',
dest='Unicode' if idn_rewrite == 'unicode' else 'IDNA',
exc2003=exc2003,
exc2008=exc2008,
))
return u'.'.join(parts)
def _adjust_idn_email(value, idn_rewrite):
idx = value.find(u'@')
if idx < 0:
return value
return u'{0}@{1}'.format(value[:idx], _adjust_idn(value[idx + 1:], idn_rewrite))
def _adjust_idn_url(value, idn_rewrite):
url = urlparse(value)
host = _adjust_idn(url.hostname, idn_rewrite)
if url.username is not None and url.password is not None:
host = u'{0}:{1}@{2}'.format(url.username, url.password, host)
elif url.username is not None:
host = u'{0}@{1}'.format(url.username, host)
if url.port is not None:
host = u'{0}:{1}'.format(host, url.port)
return urlunparse(
ParseResult(scheme=url.scheme, netloc=host, path=url.path, params=url.params, query=url.query, fragment=url.fragment))
def cryptography_get_name(name, what='Subject Alternative Name'):
'''
Given a name string, returns a cryptography x509.GeneralName object.
@@ -235,16 +453,16 @@ def cryptography_get_name(name, what='Subject Alternative Name'):
'''
try:
if name.startswith('DNS:'):
return x509.DNSName(to_text(name[4:]))
return x509.DNSName(_adjust_idn(to_text(name[4:]), 'idna'))
if name.startswith('IP:'):
address = to_text(name[3:])
if '/' in address:
return x509.IPAddress(ipaddress.ip_network(address))
return x509.IPAddress(ipaddress.ip_address(address))
if name.startswith('email:'):
return x509.RFC822Name(to_text(name[6:]))
return x509.RFC822Name(_adjust_idn_email(to_text(name[6:]), 'idna'))
if name.startswith('URI:'):
return x509.UniformResourceIdentifier(to_text(name[4:]))
return x509.UniformResourceIdentifier(_adjust_idn_url(to_text(name[4:]), 'idna'))
if name.startswith('RID:'):
m = re.match(r'^([0-9]+(?:\.[0-9]+)*)$', to_text(name[4:]))
if not m:
@@ -268,7 +486,7 @@ def cryptography_get_name(name, what='Subject Alternative Name'):
b_value = serialize_asn1_string_as_der(value)
return x509.OtherName(x509.ObjectIdentifier(oid), b_value)
if name.startswith('dirName:'):
return x509.DirectoryName(x509.Name(_parse_dn(to_text(name[8:]))))
return x509.DirectoryName(x509.Name(reversed(_parse_dn(to_bytes(name[8:])))))
except Exception as e:
raise OpenSSLObjectError('Cannot parse {what} "{name}": {error}'.format(name=name, what=what, error=e))
if ':' not in name:
@@ -280,38 +498,45 @@ def _dn_escape_value(value):
'''
Escape Distinguished Name's attribute value.
'''
value = value.replace('\\', '\\\\')
for ch in [',', '#', '+', '<', '>', ';', '"', '=', '/']:
value = value.replace(ch, '\\%s' % ch)
if value.startswith(' '):
value = r'\ ' + value[1:]
value = value.replace(u'\\', u'\\\\')
for ch in [u',', u'+', u'<', u'>', u';', u'"']:
value = value.replace(ch, u'\\%s' % ch)
value = value.replace(u'\0', u'\\00')
if value.startswith((u' ', u'#')):
value = u'\\%s' % value[0] + value[1:]
if value.endswith(u' '):
value = value[:-1] + u'\\ '
return value
def cryptography_decode_name(name):
def cryptography_decode_name(name, idn_rewrite='ignore'):
'''
Given a cryptography x509.GeneralName object, returns a string.
Raises an OpenSSLObjectError if the name is not supported.
'''
if idn_rewrite not in ('ignore', 'idna', 'unicode'):
raise AssertionError('idn_rewrite must be one of "ignore", "idna", or "unicode"')
if isinstance(name, x509.DNSName):
return 'DNS:{0}'.format(name.value)
return u'DNS:{0}'.format(_adjust_idn(name.value, idn_rewrite))
if isinstance(name, x509.IPAddress):
if isinstance(name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network)):
return 'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen)
return 'IP:{0}'.format(name.value.compressed)
return u'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen)
return u'IP:{0}'.format(name.value.compressed)
if isinstance(name, x509.RFC822Name):
return 'email:{0}'.format(name.value)
return u'email:{0}'.format(_adjust_idn_email(name.value, idn_rewrite))
if isinstance(name, x509.UniformResourceIdentifier):
return 'URI:{0}'.format(name.value)
return u'URI:{0}'.format(_adjust_idn_url(name.value, idn_rewrite))
if isinstance(name, x509.DirectoryName):
return 'dirName:' + ''.join([
'/{0}={1}'.format(cryptography_oid_to_name(attribute.oid, short=True), _dn_escape_value(attribute.value))
for attribute in name.value
# According to https://datatracker.ietf.org/doc/html/rfc4514.html#section-2.1 the
# list needs to be reversed, and joined by commas
return u'dirName:' + ','.join([
u'{0}={1}'.format(to_text(cryptography_oid_to_name(attribute.oid, short=True)), _dn_escape_value(attribute.value))
for attribute in reversed(list(name.value))
])
if isinstance(name, x509.RegisteredID):
return 'RID:{0}'.format(name.value.dotted_string)
return u'RID:{0}'.format(name.value.dotted_string)
if isinstance(name, x509.OtherName):
return 'otherName:{0};{1}'.format(name.type_id.dotted_string, _get_hex(name.value))
return u'otherName:{0};{1}'.format(name.type_id.dotted_string, _get_hex(name.value))
raise OpenSSLObjectError('Cannot decode name "{0}"'.format(name))
@@ -401,32 +626,75 @@ def cryptography_key_needs_digest_for_signing(key):
return True
def _compare_public_keys(key1, key2, clazz):
a = isinstance(key1, clazz)
b = isinstance(key2, clazz)
if not (a or b):
return None
if not a or not b:
return False
a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
return a == b
def cryptography_compare_public_keys(key1, key2):
'''Tests whether two public keys are the same.
Needs special logic for Ed25519 and Ed448 keys, since they do not have public_numbers().
'''
if CRYPTOGRAPHY_HAS_ED25519:
a = isinstance(key1, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey)
b = isinstance(key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey)
if a or b:
if not a or not b:
return False
a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
return a == b
res = _compare_public_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey)
if res is not None:
return res
if CRYPTOGRAPHY_HAS_ED448:
a = isinstance(key1, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey)
b = isinstance(key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey)
if a or b:
if not a or not b:
return False
a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
return a == b
res = _compare_public_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey)
if res is not None:
return res
return key1.public_numbers() == key2.public_numbers()
def _compare_private_keys(key1, key2, clazz, has_no_private_bytes=False):
a = isinstance(key1, clazz)
b = isinstance(key2, clazz)
if not (a or b):
return None
if not a or not b:
return False
if has_no_private_bytes:
# We do not have the private_bytes() function - compare associated public keys
return cryptography_compare_public_keys(a.public_key(), b.public_key())
encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption()
a = key1.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, encryption_algorithm=encryption_algorithm)
b = key2.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, encryption_algorithm=encryption_algorithm)
return a == b
def cryptography_compare_private_keys(key1, key2):
'''Tests whether two private keys are the same.
Needs special logic for Ed25519, X25519, and Ed448 keys, since they do not have private_numbers().
'''
if CRYPTOGRAPHY_HAS_ED25519:
res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey)
if res is not None:
return res
if CRYPTOGRAPHY_HAS_X25519:
res = _compare_private_keys(
key1, key2, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey, has_no_private_bytes=not CRYPTOGRAPHY_HAS_X25519_FULL)
if res is not None:
return res
if CRYPTOGRAPHY_HAS_ED448:
res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey)
if res is not None:
return res
if CRYPTOGRAPHY_HAS_X448:
res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey)
if res is not None:
return res
return key1.private_numbers() == key2.private_numbers()
def cryptography_serial_number_of_cert(cert):
'''Returns cert.serial_number.
@@ -442,17 +710,111 @@ def cryptography_serial_number_of_cert(cert):
def parse_pkcs12(pkcs12_bytes, passphrase=None):
'''Returns a tuple (private_key, certificate, additional_certificates, friendly_name).
'''
if _load_key_and_certificates is None:
raise ValueError('load_key_and_certificates() not present in the current cryptography version')
private_key, certificate, additional_certificates = _load_key_and_certificates(
pkcs12_bytes, to_bytes(passphrase) if passphrase is not None else None)
if _load_pkcs12 is None and _load_key_and_certificates is None:
raise ValueError('neither load_pkcs12() nor load_key_and_certificates() present in the current cryptography version')
if passphrase is not None:
passphrase = to_bytes(passphrase)
# Main code for cryptography 36.0.0 and forward
if _load_pkcs12 is not None:
return _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase)
if LooseVersion(cryptography.__version__) >= LooseVersion('35.0'):
return _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase)
return _parse_pkcs12_legacy(pkcs12_bytes, passphrase)
def _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase=None):
# Requires cryptography 36.0.0 or newer
pkcs12 = _load_pkcs12(pkcs12_bytes, passphrase)
additional_certificates = [cert.certificate for cert in pkcs12.additional_certs]
private_key = pkcs12.key
certificate = None
friendly_name = None
if pkcs12.cert:
certificate = pkcs12.cert.certificate
friendly_name = pkcs12.cert.friendly_name
return private_key, certificate, additional_certificates, friendly_name
def _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase=None):
# Backwards compatibility code for cryptography 35.x
private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase)
friendly_name = None
if certificate:
# See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238
maybe_name = certificate._backend._lib.X509_alias_get0(
certificate._x509, certificate._backend._ffi.NULL)
if maybe_name != certificate._backend._ffi.NULL:
friendly_name = certificate._backend._ffi.string(maybe_name)
backend = default_backend()
# This code basically does what load_key_and_certificates() does, but without error-checking.
# Since load_key_and_certificates succeeded, it should not fail.
pkcs12 = backend._ffi.gc(
backend._lib.d2i_PKCS12_bio(backend._bytes_to_bio(pkcs12_bytes).bio, backend._ffi.NULL),
backend._lib.PKCS12_free)
certificate_x509_ptr = backend._ffi.new("X509 **")
with backend._zeroed_null_terminated_buf(to_bytes(passphrase) if passphrase is not None else None) as passphrase_buffer:
backend._lib.PKCS12_parse(
pkcs12,
passphrase_buffer,
backend._ffi.new("EVP_PKEY **"),
certificate_x509_ptr,
backend._ffi.new("Cryptography_STACK_OF_X509 **"))
if certificate_x509_ptr[0] != backend._ffi.NULL:
maybe_name = backend._lib.X509_alias_get0(certificate_x509_ptr[0], backend._ffi.NULL)
if maybe_name != backend._ffi.NULL:
friendly_name = backend._ffi.string(maybe_name)
return private_key, certificate, additional_certificates, friendly_name
def _parse_pkcs12_legacy(pkcs12_bytes, passphrase=None):
# Backwards compatibility code for cryptography < 35.0.0
private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase)
friendly_name = None
if certificate:
# See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238
backend = certificate._backend
maybe_name = backend._lib.X509_alias_get0(certificate._x509, backend._ffi.NULL)
if maybe_name != backend._ffi.NULL:
friendly_name = backend._ffi.string(maybe_name)
return private_key, certificate, additional_certificates, friendly_name
def cryptography_verify_signature(signature, data, hash_algorithm, signer_public_key):
'''
Check whether the given signature of the given data was signed by the given public key object.
'''
try:
if CRYPTOGRAPHY_HAS_RSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
signer_public_key.verify(signature, data, padding.PKCS1v15(), hash_algorithm)
return True
if CRYPTOGRAPHY_HAS_EC_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
signer_public_key.verify(signature, data, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hash_algorithm))
return True
if CRYPTOGRAPHY_HAS_DSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey):
signer_public_key.verify(signature, data, hash_algorithm)
return True
if CRYPTOGRAPHY_HAS_ED25519_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey):
signer_public_key.verify(signature, data)
return True
if CRYPTOGRAPHY_HAS_ED448_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey):
signer_public_key.verify(signature, data)
return True
raise OpenSSLObjectError(u'Unsupported public key type {0}'.format(type(signer_public_key)))
except InvalidSignature:
return False
def cryptography_verify_certificate_signature(certificate, signer_public_key):
'''
Check whether the given X509 certificate object was signed by the given public key object.
'''
return cryptography_verify_signature(
certificate.signature,
certificate.tbs_certificate_bytes,
certificate.signature_hash_algorithm,
signer_public_key
)

View File

@@ -50,7 +50,7 @@ def quick_is_not_prime(n):
'''Does some quick checks to see if we can poke a hole into the primality of n.
A result of `False` does **not** mean that the number is prime; it just means
that we couldn't detect quickly whether it is not prime.
that we could not detect quickly whether it is not prime.
'''
if n <= 2:
return True

View File

@@ -11,11 +11,11 @@ __metaclass__ = type
import abc
import traceback
from distutils.version import LooseVersion
from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
@@ -38,18 +38,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
MINIMAL_PYOPENSSL_VERSION = '0.15'
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
except ImportError:
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
PYOPENSSL_FOUND = True
CRYPTOGRAPHY_IMP_ERR = None
CRYPTOGRAPHY_VERSION = None
@@ -75,6 +63,7 @@ class CertificateBackend(object):
self.backend = backend
self.force = module.params['force']
self.ignore_timestamps = module.params['ignore_timestamps']
self.privatekey_path = module.params['privatekey_path']
self.privatekey_content = module.params['privatekey_content']
if self.privatekey_content is not None:
@@ -173,43 +162,12 @@ class CertificateBackend(object):
def _check_privatekey(self):
"""Check whether provided parameters match, assuming self.existing_certificate and self.privatekey have been populated."""
if self.backend == 'pyopenssl':
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD)
ctx.use_privatekey(self.privatekey)
ctx.use_certificate(self.existing_certificate)
try:
ctx.check_privatekey()
return True
except OpenSSL.SSL.Error:
return False
elif self.backend == 'cryptography':
if self.backend == 'cryptography':
return cryptography_compare_public_keys(self.existing_certificate.public_key(), self.privatekey.public_key())
def _check_csr(self):
"""Check whether provided parameters match, assuming self.existing_certificate and self.csr have been populated."""
if self.backend == 'pyopenssl':
# Verify that CSR is signed by certificate's private key
try:
self.csr.verify(self.existing_certificate.get_pubkey())
except OpenSSL.crypto.Error:
return False
# Check subject
if self.check_csr_subject and self.csr.get_subject() != self.existing_certificate.get_subject():
return False
# Check extensions
if not self.check_csr_extensions:
return True
csr_extensions = self.csr.get_extensions()
cert_extension_count = self.existing_certificate.get_extension_count()
if len(csr_extensions) != cert_extension_count:
return False
for extension_number in range(0, cert_extension_count):
cert_extension = self.existing_certificate.get_extension(extension_number)
csr_extension = filter(lambda extension: extension.get_short_name() == cert_extension.get_short_name(), csr_extensions)
if cert_extension.get_data() != list(csr_extension)[0].get_data():
return False
return True
elif self.backend == 'cryptography':
if self.backend == 'cryptography':
# Verify that CSR is signed by certificate's private key
if not self.csr.is_signature_valid:
return False
@@ -244,10 +202,6 @@ class CertificateBackend(object):
def _check_subject_key_identifier(self):
"""Check whether Subject Key Identifier matches, assuming self.existing_certificate has been populated."""
if self.backend != 'cryptography':
# We do not support SKI with pyOpenSSL backend
return True
# Get hold of certificate's SKI
try:
ext = self.existing_certificate.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
@@ -265,12 +219,12 @@ class CertificateBackend(object):
if ext.value.digest != x509.SubjectKeyIdentifier.from_public_key(self.existing_certificate.public_key()).digest:
return False
else:
# If CSR had SKI and we didn't ignore it ('create_if_not_provided'), compare SKIs
# 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):
def needs_regeneration(self, not_before=None, not_after=None):
"""Check whether a regeneration is necessary."""
if self.force or self.existing_certificate_bytes is None:
return True
@@ -294,6 +248,15 @@ class CertificateBackend(object):
if self.create_subject_key_identifier != 'never_create' and not self._check_subject_key_identifier():
return True
# Check not before
if not_before is not None and not self.ignore_timestamps:
if self.existing_certificate.not_valid_before != not_before:
return True
# Check not after
if not_after is not None and not self.ignore_timestamps:
if self.existing_certificate.not_valid_after != not_after:
return True
return False
def dump(self, include_certificate):
@@ -328,10 +291,6 @@ class CertificateProvider(object):
def needs_version_two_certs(self, module):
"""Whether the provider needs to create a version 2 certificate."""
def needs_pyopenssl_get_extensions(self, module):
"""Whether the provider needs to use get_extensions() with pyOpenSSL."""
return True
@abc.abstractmethod
def create_backend(self, module, backend):
"""Create an implementation for a backend.
@@ -352,45 +311,22 @@ def select_backend(module, backend, provider):
if backend == 'auto':
# Detect what backend we can use
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
# If cryptography is available we'll use it
if can_use_cryptography:
backend = 'cryptography'
elif can_use_pyopenssl:
backend = 'pyopenssl'
if provider.needs_version_two_certs(module):
module.warn('crypto backend forced to pyopenssl. The cryptography library does not support v2 certificates')
backend = 'pyopenssl'
# Fail if no backend has been found
if backend == 'auto':
module.fail_json(msg=("Can't detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION))
module.fail_json(msg=("Cannot detect the required Python library "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == 'pyopenssl':
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.0.0', collection_name='community.crypto')
if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR)
if provider.needs_pyopenssl_get_extensions(module):
try:
getattr(crypto.X509Req, 'get_extensions')
except AttributeError:
module.fail_json(msg='You need to have PyOpenSSL>=0.15')
elif backend == 'cryptography':
if backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
if provider.needs_version_two_certs(module):
module.fail_json(msg='The cryptography backend does not support v2 certificates, '
'use select_crypto_backend=pyopenssl for v2 certificates')
module.fail_json(msg='The cryptography backend does not support v2 certificates')
return provider.create_backend(module, backend)
@@ -402,7 +338,8 @@ def get_certificate_argument_spec():
force=dict(type='bool', default=False,),
csr_path=dict(type='path'),
csr_content=dict(type='str'),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
ignore_timestamps=dict(type='bool', default=True),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
# General properties of a certificate
privatekey_path=dict(type='path'),

View File

@@ -1,664 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import abc
import datetime
from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
parse_name_field,
get_relative_time_option,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
cryptography_compare_public_keys,
cryptography_get_name,
cryptography_name_to_oid,
cryptography_parse_key_usage_params,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
pyopenssl_normalize_name_attribute,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
CertificateBackend,
CertificateProvider,
)
try:
import OpenSSL
from OpenSSL import crypto
except ImportError:
pass
try:
import cryptography
from cryptography import x509
from cryptography.x509 import NameAttribute, Name
except ImportError:
pass
def compare_sets(subset, superset, equality=False):
if equality:
return set(subset) == set(superset)
else:
return all(x in superset for x in subset)
def compare_dicts(subset, superset, equality=False):
if equality:
return subset == superset
else:
return all(superset.get(x) == v for x, v in subset.items())
NO_EXTENSION = 'no extension'
class AssertOnlyCertificateBackend(CertificateBackend):
def __init__(self, module, backend):
super(AssertOnlyCertificateBackend, self).__init__(module, backend)
self.signature_algorithms = module.params['signature_algorithms']
if module.params['subject']:
self.subject = parse_name_field(module.params['subject'])
else:
self.subject = []
self.subject_strict = module.params['subject_strict']
if module.params['issuer']:
self.issuer = parse_name_field(module.params['issuer'])
else:
self.issuer = []
self.issuer_strict = module.params['issuer_strict']
self.has_expired = module.params['has_expired']
self.version = module.params['version']
self.key_usage = module.params['key_usage']
self.key_usage_strict = module.params['key_usage_strict']
self.extended_key_usage = module.params['extended_key_usage']
self.extended_key_usage_strict = module.params['extended_key_usage_strict']
self.subject_alt_name = module.params['subject_alt_name']
self.subject_alt_name_strict = module.params['subject_alt_name_strict']
self.not_before = module.params['not_before']
self.not_after = module.params['not_after']
self.valid_at = module.params['valid_at']
self.invalid_at = module.params['invalid_at']
self.valid_in = module.params['valid_in']
if self.valid_in and not self.valid_in.startswith("+") and not self.valid_in.startswith("-"):
try:
int(self.valid_in)
except ValueError:
module.fail_json(msg='The supplied value for "valid_in" (%s) is not an integer or a valid timespec' % self.valid_in)
self.valid_in = "+" + self.valid_in + "s"
# Load objects
self._ensure_private_key_loaded()
self._ensure_csr_loaded()
@abc.abstractmethod
def _validate_privatekey(self):
pass
@abc.abstractmethod
def _validate_csr_signature(self):
pass
@abc.abstractmethod
def _validate_csr_subject(self):
pass
@abc.abstractmethod
def _validate_csr_extensions(self):
pass
@abc.abstractmethod
def _validate_signature_algorithms(self):
pass
@abc.abstractmethod
def _validate_subject(self):
pass
@abc.abstractmethod
def _validate_issuer(self):
pass
@abc.abstractmethod
def _validate_has_expired(self):
pass
@abc.abstractmethod
def _validate_version(self):
pass
@abc.abstractmethod
def _validate_key_usage(self):
pass
@abc.abstractmethod
def _validate_extended_key_usage(self):
pass
@abc.abstractmethod
def _validate_subject_alt_name(self):
pass
@abc.abstractmethod
def _validate_not_before(self):
pass
@abc.abstractmethod
def _validate_not_after(self):
pass
@abc.abstractmethod
def _validate_valid_at(self):
pass
@abc.abstractmethod
def _validate_invalid_at(self):
pass
@abc.abstractmethod
def _validate_valid_in(self):
pass
def assertonly(self):
messages = []
if self.privatekey_path is not None or self.privatekey_content is not None:
if not self._validate_privatekey():
messages.append(
'Certificate and private key %s do not match' %
(self.privatekey_path or '(provided in module options)')
)
if self.csr_path is not None or self.csr_content is not None:
if not self._validate_csr_signature():
messages.append(
'Certificate and CSR %s do not match: private key mismatch' %
(self.csr_path or '(provided in module options)')
)
if not self._validate_csr_subject():
messages.append(
'Certificate and CSR %s do not match: subject mismatch' %
(self.csr_path or '(provided in module options)')
)
if not self._validate_csr_extensions():
messages.append(
'Certificate and CSR %s do not match: extensions mismatch' %
(self.csr_path or '(provided in module options)')
)
if self.signature_algorithms is not None:
wrong_alg = self._validate_signature_algorithms()
if wrong_alg:
messages.append(
'Invalid signature algorithm (got %s, expected one of %s)' %
(wrong_alg, self.signature_algorithms)
)
if self.subject is not None:
failure = self._validate_subject()
if failure:
dummy, cert_subject = failure
messages.append(
'Invalid subject component (got %s, expected all of %s to be present)' %
(cert_subject, self.subject)
)
if self.issuer is not None:
failure = self._validate_issuer()
if failure:
dummy, cert_issuer = failure
messages.append(
'Invalid issuer component (got %s, expected all of %s to be present)' % (cert_issuer, self.issuer)
)
if self.has_expired is not None:
cert_expired = self._validate_has_expired()
if cert_expired != self.has_expired:
messages.append(
'Certificate expiration check failed (certificate expiration is %s, expected %s)' %
(cert_expired, self.has_expired)
)
if self.version is not None:
cert_version = self._validate_version()
if cert_version != self.version:
messages.append(
'Invalid certificate version number (got %s, expected %s)' %
(cert_version, self.version)
)
if self.key_usage is not None:
failure = self._validate_key_usage()
if failure == NO_EXTENSION:
messages.append('Found no keyUsage extension')
elif failure:
dummy, cert_key_usage = failure
messages.append(
'Invalid keyUsage components (got %s, expected all of %s to be present)' %
(cert_key_usage, self.key_usage)
)
if self.extended_key_usage is not None:
failure = self._validate_extended_key_usage()
if failure == NO_EXTENSION:
messages.append('Found no extendedKeyUsage extension')
elif failure:
dummy, ext_cert_key_usage = failure
messages.append(
'Invalid extendedKeyUsage component (got %s, expected all of %s to be present)' % (ext_cert_key_usage, self.extended_key_usage)
)
if self.subject_alt_name is not None:
failure = self._validate_subject_alt_name()
if failure == NO_EXTENSION:
messages.append('Found no subjectAltName extension')
elif failure:
dummy, cert_san = failure
messages.append(
'Invalid subjectAltName component (got %s, expected all of %s to be present)' %
(cert_san, self.subject_alt_name)
)
if self.not_before is not None:
cert_not_valid_before = self._validate_not_before()
if cert_not_valid_before != get_relative_time_option(self.not_before, 'not_before', backend=self.backend):
messages.append(
'Invalid not_before component (got %s, expected %s to be present)' %
(cert_not_valid_before, self.not_before)
)
if self.not_after is not None:
cert_not_valid_after = self._validate_not_after()
if cert_not_valid_after != get_relative_time_option(self.not_after, 'not_after', backend=self.backend):
messages.append(
'Invalid not_after component (got %s, expected %s to be present)' %
(cert_not_valid_after, self.not_after)
)
if self.valid_at is not None:
not_before, valid_at, not_after = self._validate_valid_at()
if not (not_before <= valid_at <= not_after):
messages.append(
'Certificate is not valid for the specified date (%s) - not_before: %s - not_after: %s' %
(self.valid_at, not_before, not_after)
)
if self.invalid_at is not None:
not_before, invalid_at, not_after = self._validate_invalid_at()
if not_before <= invalid_at <= not_after:
messages.append(
'Certificate is not invalid for the specified date (%s) - not_before: %s - not_after: %s' %
(self.invalid_at, not_before, not_after)
)
if self.valid_in is not None:
not_before, valid_in, not_after = self._validate_valid_in()
if not not_before <= valid_in <= not_after:
messages.append(
'Certificate is not valid in %s from now (that would be %s) - not_before: %s - not_after: %s' %
(self.valid_in, valid_in, not_before, not_after)
)
return messages
def needs_regeneration(self):
self._ensure_existing_certificate_loaded()
if self.existing_certificate is None:
self.messages = ['Certificate not provided']
else:
self.messages = self.assertonly()
return len(self.messages) != 0
def generate_certificate(self):
self.module.fail_json(msg=' | '.join(self.messages))
def get_certificate_data(self):
return self.existing_certificate_bytes
class AssertOnlyCertificateBackendCryptography(AssertOnlyCertificateBackend):
"""Validate the supplied cert, using the cryptography backend"""
def __init__(self, module):
super(AssertOnlyCertificateBackendCryptography, self).__init__(module, 'cryptography')
def _validate_privatekey(self):
return cryptography_compare_public_keys(self.existing_certificate.public_key(), self.privatekey.public_key())
def _validate_csr_signature(self):
if not self.csr.is_signature_valid:
return False
return cryptography_compare_public_keys(self.csr.public_key(), self.existing_certificate.public_key())
def _validate_csr_subject(self):
return self.csr.subject == self.existing_certificate.subject
def _validate_csr_extensions(self):
cert_exts = self.existing_certificate.extensions
csr_exts = self.csr.extensions
if len(cert_exts) != len(csr_exts):
return False
for cert_ext in cert_exts:
try:
csr_ext = csr_exts.get_extension_for_oid(cert_ext.oid)
if cert_ext != csr_ext:
return False
except cryptography.x509.ExtensionNotFound as dummy:
return False
return True
def _validate_signature_algorithms(self):
if self.existing_certificate.signature_algorithm_oid._name not in self.signature_algorithms:
return self.existing_certificate.signature_algorithm_oid._name
def _validate_subject(self):
expected_subject = Name([NameAttribute(oid=cryptography_name_to_oid(sub[0]), value=to_text(sub[1]))
for sub in self.subject])
cert_subject = self.existing_certificate.subject
if not compare_sets(expected_subject, cert_subject, self.subject_strict):
return expected_subject, cert_subject
def _validate_issuer(self):
expected_issuer = Name([NameAttribute(oid=cryptography_name_to_oid(iss[0]), value=to_text(iss[1]))
for iss in self.issuer])
cert_issuer = self.existing_certificate.issuer
if not compare_sets(expected_issuer, cert_issuer, self.issuer_strict):
return self.issuer, cert_issuer
def _validate_has_expired(self):
cert_not_after = self.existing_certificate.not_valid_after
cert_expired = cert_not_after < datetime.datetime.utcnow()
return cert_expired
def _validate_version(self):
if self.existing_certificate.version == x509.Version.v1:
return 1
if self.existing_certificate.version == x509.Version.v3:
return 3
return "unknown"
def _validate_key_usage(self):
try:
current_key_usage = self.existing_certificate.extensions.get_extension_for_class(x509.KeyUsage).value
test_key_usage = dict(
digital_signature=current_key_usage.digital_signature,
content_commitment=current_key_usage.content_commitment,
key_encipherment=current_key_usage.key_encipherment,
data_encipherment=current_key_usage.data_encipherment,
key_agreement=current_key_usage.key_agreement,
key_cert_sign=current_key_usage.key_cert_sign,
crl_sign=current_key_usage.crl_sign,
encipher_only=False,
decipher_only=False
)
if test_key_usage['key_agreement']:
test_key_usage.update(dict(
encipher_only=current_key_usage.encipher_only,
decipher_only=current_key_usage.decipher_only
))
key_usages = cryptography_parse_key_usage_params(self.key_usage)
if not compare_dicts(key_usages, test_key_usage, self.key_usage_strict):
return self.key_usage, [k for k, v in test_key_usage.items() if v is True]
except cryptography.x509.ExtensionNotFound:
# This is only bad if the user specified a non-empty list
if self.key_usage:
return NO_EXTENSION
def _validate_extended_key_usage(self):
try:
current_ext_keyusage = self.existing_certificate.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value
usages = [cryptography_name_to_oid(usage) for usage in self.extended_key_usage]
expected_ext_keyusage = x509.ExtendedKeyUsage(usages)
if not compare_sets(expected_ext_keyusage, current_ext_keyusage, self.extended_key_usage_strict):
return [eku.value for eku in expected_ext_keyusage], [eku.value for eku in current_ext_keyusage]
except cryptography.x509.ExtensionNotFound:
# This is only bad if the user specified a non-empty list
if self.extended_key_usage:
return NO_EXTENSION
def _validate_subject_alt_name(self):
try:
current_san = self.existing_certificate.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
expected_san = [cryptography_get_name(san) for san in self.subject_alt_name]
if not compare_sets(expected_san, current_san, self.subject_alt_name_strict):
return self.subject_alt_name, current_san
except cryptography.x509.ExtensionNotFound:
# This is only bad if the user specified a non-empty list
if self.subject_alt_name:
return NO_EXTENSION
def _validate_not_before(self):
return self.existing_certificate.not_valid_before
def _validate_not_after(self):
return self.existing_certificate.not_valid_after
def _validate_valid_at(self):
rt = get_relative_time_option(self.valid_at, 'valid_at', backend=self.backend)
return self.existing_certificate.not_valid_before, rt, self.existing_certificate.not_valid_after
def _validate_invalid_at(self):
rt = get_relative_time_option(self.invalid_at, 'invalid_at', backend=self.backend)
return self.existing_certificate.not_valid_before, rt, self.existing_certificate.not_valid_after
def _validate_valid_in(self):
valid_in_date = get_relative_time_option(self.valid_in, "valid_in", backend=self.backend)
return self.existing_certificate.not_valid_before, valid_in_date, self.existing_certificate.not_valid_after
class AssertOnlyCertificateBackendPyOpenSSL(AssertOnlyCertificateBackend):
"""validate the supplied certificate."""
def __init__(self, module):
super(AssertOnlyCertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
# Ensure inputs are properly sanitized before comparison.
for param in ['signature_algorithms', 'key_usage', 'extended_key_usage',
'subject_alt_name', 'subject', 'issuer', 'not_before',
'not_after', 'valid_at', 'invalid_at']:
attr = getattr(self, param)
if isinstance(attr, list) and attr:
if isinstance(attr[0], str):
setattr(self, param, [to_bytes(item) for item in attr])
elif isinstance(attr[0], tuple):
setattr(self, param, [(to_bytes(item[0]), to_bytes(item[1])) for item in attr])
elif isinstance(attr, tuple):
setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items()))
elif isinstance(attr, dict):
setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items()))
elif isinstance(attr, str):
setattr(self, param, to_bytes(attr))
def _validate_privatekey(self):
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD)
ctx.use_privatekey(self.privatekey)
ctx.use_certificate(self.existing_certificate)
try:
ctx.check_privatekey()
return True
except OpenSSL.SSL.Error:
return False
def _validate_csr_signature(self):
try:
self.csr.verify(self.existing_certificate.get_pubkey())
except OpenSSL.crypto.Error:
return False
def _validate_csr_subject(self):
if self.csr.get_subject() != self.existing_certificate.get_subject():
return False
def _validate_csr_extensions(self):
csr_extensions = self.csr.get_extensions()
cert_extension_count = self.existing_certificate.get_extension_count()
if len(csr_extensions) != cert_extension_count:
return False
for extension_number in range(0, cert_extension_count):
cert_extension = self.existing_certificate.get_extension(extension_number)
csr_extension = filter(lambda extension: extension.get_short_name() == cert_extension.get_short_name(), csr_extensions)
if cert_extension.get_data() != list(csr_extension)[0].get_data():
return False
return True
def _validate_signature_algorithms(self):
if self.existing_certificate.get_signature_algorithm() not in self.signature_algorithms:
return self.existing_certificate.get_signature_algorithm()
def _validate_subject(self):
expected_subject = [(OpenSSL._util.lib.OBJ_txt2nid(sub[0]), sub[1]) for sub in self.subject]
cert_subject = self.existing_certificate.get_subject().get_components()
current_subject = [(OpenSSL._util.lib.OBJ_txt2nid(sub[0]), sub[1]) for sub in cert_subject]
if not compare_sets(expected_subject, current_subject, self.subject_strict):
return expected_subject, current_subject
def _validate_issuer(self):
expected_issuer = [(OpenSSL._util.lib.OBJ_txt2nid(iss[0]), iss[1]) for iss in self.issuer]
cert_issuer = self.existing_certificate.get_issuer().get_components()
current_issuer = [(OpenSSL._util.lib.OBJ_txt2nid(iss[0]), iss[1]) for iss in cert_issuer]
if not compare_sets(expected_issuer, current_issuer, self.issuer_strict):
return self.issuer, cert_issuer
def _validate_has_expired(self):
# The following 3 lines are the same as the current PyOpenSSL code for cert.has_expired().
# Older version of PyOpenSSL have a buggy implementation,
# to avoid issues with those we added the code from a more recent release here.
time_string = to_native(self.existing_certificate.get_notAfter())
not_after = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
cert_expired = not_after < datetime.datetime.utcnow()
return cert_expired
def _validate_version(self):
# Version numbers in certs are off by one:
# v1: 0, v2: 1, v3: 2 ...
return self.existing_certificate.get_version() + 1
def _validate_key_usage(self):
found = False
for extension_idx in range(0, self.existing_certificate.get_extension_count()):
extension = self.existing_certificate.get_extension(extension_idx)
if extension.get_short_name() == b'keyUsage':
found = True
expected_extension = crypto.X509Extension(b"keyUsage", False, b', '.join(self.key_usage))
key_usage = [usage.strip() for usage in to_text(expected_extension, errors='surrogate_or_strict').split(',')]
current_ku = [usage.strip() for usage in to_text(extension, errors='surrogate_or_strict').split(',')]
if not compare_sets(key_usage, current_ku, self.key_usage_strict):
return self.key_usage, str(extension).split(', ')
if not found:
# This is only bad if the user specified a non-empty list
if self.key_usage:
return NO_EXTENSION
def _validate_extended_key_usage(self):
found = False
for extension_idx in range(0, self.existing_certificate.get_extension_count()):
extension = self.existing_certificate.get_extension(extension_idx)
if extension.get_short_name() == b'extendedKeyUsage':
found = True
extKeyUsage = [OpenSSL._util.lib.OBJ_txt2nid(keyUsage) for keyUsage in self.extended_key_usage]
current_xku = [OpenSSL._util.lib.OBJ_txt2nid(usage.strip()) for usage in
to_bytes(extension, errors='surrogate_or_strict').split(b',')]
if not compare_sets(extKeyUsage, current_xku, self.extended_key_usage_strict):
return self.extended_key_usage, str(extension).split(', ')
if not found:
# This is only bad if the user specified a non-empty list
if self.extended_key_usage:
return NO_EXTENSION
def _validate_subject_alt_name(self):
found = False
for extension_idx in range(0, self.existing_certificate.get_extension_count()):
extension = self.existing_certificate.get_extension(extension_idx)
if extension.get_short_name() == b'subjectAltName':
found = True
l_altnames = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
to_text(extension, errors='surrogate_or_strict').split(', ')]
sans = [pyopenssl_normalize_name_attribute(to_text(san, errors='surrogate_or_strict')) for san in self.subject_alt_name]
if not compare_sets(sans, l_altnames, self.subject_alt_name_strict):
return self.subject_alt_name, l_altnames
if not found:
# This is only bad if the user specified a non-empty list
if self.subject_alt_name:
return NO_EXTENSION
def _validate_not_before(self):
return self.existing_certificate.get_notBefore()
def _validate_not_after(self):
return self.existing_certificate.get_notAfter()
def _validate_valid_at(self):
rt = get_relative_time_option(self.valid_at, "valid_at", backend=self.backend)
rt = to_bytes(rt, errors='surrogate_or_strict')
return self.existing_certificate.get_notBefore(), rt, self.existing_certificate.get_notAfter()
def _validate_invalid_at(self):
rt = get_relative_time_option(self.invalid_at, "invalid_at", backend=self.backend)
rt = to_bytes(rt, errors='surrogate_or_strict')
return self.existing_certificate.get_notBefore(), rt, self.existing_certificate.get_notAfter()
def _validate_valid_in(self):
valid_in_asn1 = get_relative_time_option(self.valid_in, "valid_in", backend=self.backend)
valid_in_date = to_bytes(valid_in_asn1, errors='surrogate_or_strict')
return self.existing_certificate.get_notBefore(), valid_in_date, self.existing_certificate.get_notAfter()
class AssertOnlyCertificateProvider(CertificateProvider):
def validate_module_args(self, module):
module.deprecate("The 'assertonly' provider is deprecated; please see the examples of "
"the 'x509_certificate' module on how to replace it with other modules",
version='2.0.0', collection_name='community.crypto')
def needs_version_two_certs(self, module):
return False
def create_backend(self, module, backend):
if backend == 'cryptography':
return AssertOnlyCertificateBackendCryptography(module)
if backend == 'pyopenssl':
return AssertOnlyCertificateBackendPyOpenSSL(module)
def add_assertonly_provider_to_argument_spec(argument_spec):
argument_spec.argument_spec['provider']['choices'].append('assertonly')
argument_spec.argument_spec.update(dict(
signature_algorithms=dict(type='list', elements='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
subject=dict(type='dict', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
subject_strict=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'),
issuer=dict(type='dict', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
issuer_strict=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'),
has_expired=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'),
version=dict(type='int', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
key_usage=dict(type='list', elements='str', aliases=['keyUsage'],
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
key_usage_strict=dict(type='bool', default=False, aliases=['keyUsage_strict'],
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
extended_key_usage=dict(type='list', elements='str', aliases=['extendedKeyUsage'],
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
extended_key_usage_strict=dict(type='bool', default=False, aliases=['extendedKeyUsage_strict'],
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName'],
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
subject_alt_name_strict=dict(type='bool', default=False, aliases=['subjectAltName_strict'],
removed_in_version='2.0.0', removed_from_collection='community.crypto'),
not_before=dict(type='str', aliases=['notBefore'], removed_in_version='2.0.0', removed_from_collection='community.crypto'),
not_after=dict(type='str', aliases=['notAfter'], removed_in_version='2.0.0', removed_from_collection='community.crypto'),
valid_at=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
invalid_at=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
valid_in=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'),
))

View File

@@ -58,18 +58,7 @@ class EntrustCertificateBackend(CertificateBackend):
# We want to always force behavior of trying to use the organization provided in the CSR.
# To that end we need to parse out the organization from the CSR.
self.csr_org = None
if self.backend == 'pyopenssl':
csr_subject = self.csr.get_subject()
csr_subject_components = csr_subject.get_components()
for k, v in csr_subject_components:
if k.upper() == 'O':
# Entrust does not support multiple validated organizations in a single certificate
if self.csr_org is not None:
self.module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations "
"found in Subject DN: '{0}'. ".format(csr_subject)))
else:
self.csr_org = v
elif self.backend == 'cryptography':
if self.backend == 'cryptography':
csr_subject_orgs = self.csr.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)
if len(csr_subject_orgs) == 1:
self.csr_org = csr_subject_orgs[0].value
@@ -162,11 +151,7 @@ class EntrustCertificateBackend(CertificateBackend):
if self.existing_certificate:
serial_number = None
expiry = None
if self.backend == 'pyopenssl':
serial_number = "{0:X}".format(self.existing_certificate.get_serial_number())
time_string = to_native(self.existing_certificate.get_notAfter())
expiry = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
elif self.backend == 'cryptography':
if self.backend == 'cryptography':
serial_number = "{0:X}".format(cryptography_serial_number_of_cert(self.existing_certificate))
expiry = self.existing_certificate.not_valid_after

View File

@@ -12,14 +12,13 @@ __metaclass__ = type
import abc
import binascii
import datetime
import re
import traceback
from distutils.version import LooseVersion
from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_certificate,
@@ -33,37 +32,11 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
cryptography_serial_number_of_cert,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
pyopenssl_get_extensions_from_cert,
pyopenssl_normalize_name,
pyopenssl_normalize_name_attribute,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
get_publickey_info,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
MINIMAL_PYOPENSSL_VERSION = '0.15'
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
# OpenSSL 1.1.0 or newer
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
else:
# OpenSSL 1.0.x or older
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
except ImportError:
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
PYOPENSSL_FOUND = True
CRYPTOGRAPHY_IMP_ERR = None
try:
@@ -209,20 +182,19 @@ class CertificateInfoRetrieval(object):
result['fingerprints'] = get_fingerprint_of_bytes(
self._get_der_bytes(), prefer_one=prefer_one_fingerprint)
if self.backend != 'pyopenssl':
ski = self._get_subject_key_identifier()
if ski is not None:
ski = to_native(binascii.hexlify(ski))
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
result['subject_key_identifier'] = ski
ski = self._get_subject_key_identifier()
if ski is not None:
ski = to_native(binascii.hexlify(ski))
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
result['subject_key_identifier'] = ski
aki, aci, acsn = self._get_authority_key_identifier()
if aki is not None:
aki = to_native(binascii.hexlify(aki))
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
result['authority_key_identifier'] = aki
result['authority_cert_issuer'] = aci
result['authority_cert_serial_number'] = acsn
aki, aci, acsn = self._get_authority_key_identifier()
if aki is not None:
aki = to_native(binascii.hexlify(aki))
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
result['authority_key_identifier'] = aki
result['authority_cert_issuer'] = aci
result['authority_cert_serial_number'] = acsn
result['serial_number'] = self._get_serial_number()
result['extensions_by_oid'] = self._get_all_extensions()
@@ -235,6 +207,7 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
"""Validate the supplied cert, using the cryptography backend"""
def __init__(self, module, content):
super(CertificateInfoRetrievalCryptography, self).__init__(module, 'cryptography', content)
self.name_encoding = module.params.get('name_encoding', 'ignore')
def _get_der_bytes(self):
return self.cert.public_bytes(serialization.Encoding.DER)
@@ -337,7 +310,7 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
def _get_subject_alt_name(self):
try:
san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
result = [cryptography_decode_name(san) for san in san_ext.value]
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
@@ -369,7 +342,7 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
issuer = None
if ext.value.authority_cert_issuer is not None:
issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
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
@@ -392,136 +365,9 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
return None
class CertificateInfoRetrievalPyOpenSSL(CertificateInfoRetrieval):
"""validate the supplied certificate."""
def __init__(self, module, content):
super(CertificateInfoRetrievalPyOpenSSL, self).__init__(module, 'pyopenssl', content)
def _get_der_bytes(self):
return crypto.dump_certificate(crypto.FILETYPE_ASN1, self.cert)
def _get_signature_algorithm(self):
return to_text(self.cert.get_signature_algorithm())
def __get_name(self, name):
result = []
for sub in name.get_components():
result.append([pyopenssl_normalize_name(sub[0]), to_text(sub[1])])
return result
def _get_subject_ordered(self):
return self.__get_name(self.cert.get_subject())
def _get_issuer_ordered(self):
return self.__get_name(self.cert.get_issuer())
def _get_version(self):
# Version numbers in certs are off by one:
# v1: 0, v2: 1, v3: 2 ...
return self.cert.get_version() + 1
def _get_extension(self, short_name):
for extension_idx in range(0, self.cert.get_extension_count()):
extension = self.cert.get_extension(extension_idx)
if extension.get_short_name() == short_name:
result = [
pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',')
]
return sorted(result), bool(extension.get_critical())
return None, False
def _get_key_usage(self):
return self._get_extension(b'keyUsage')
def _get_extended_key_usage(self):
return self._get_extension(b'extendedKeyUsage')
def _get_basic_constraints(self):
return self._get_extension(b'basicConstraints')
def _get_ocsp_must_staple(self):
extensions = [self.cert.get_extension(i) for i in range(0, self.cert.get_extension_count())]
oms_ext = [
ext for ext in extensions
if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE
]
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
# Older versions of libssl don't know about OCSP Must Staple
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
if oms_ext:
return True, bool(oms_ext[0].get_critical())
else:
return None, False
def _get_subject_alt_name(self):
for extension_idx in range(0, self.cert.get_extension_count()):
extension = self.cert.get_extension(extension_idx)
if extension.get_short_name() == b'subjectAltName':
result = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
to_text(extension, errors='surrogate_or_strict').split(', ')]
return result, bool(extension.get_critical())
return None, False
def get_not_before(self):
time_string = to_native(self.cert.get_notBefore())
return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
def get_not_after(self):
time_string = to_native(self.cert.get_notAfter())
return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
def _get_public_key_pem(self):
try:
return crypto.dump_publickey(
crypto.FILETYPE_PEM,
self.cert.get_pubkey(),
)
except AttributeError:
try:
# pyOpenSSL < 16.0:
bio = crypto._new_mem_buf()
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.cert.get_pubkey()._pkey)
if rc != 1:
crypto._raise_current_error()
return crypto._bio_to_string(bio)
except AttributeError:
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
def _get_public_key_object(self):
return self.cert.get_pubkey()
def _get_subject_key_identifier(self):
# Won't be implemented
return None
def _get_authority_key_identifier(self):
# Won't be implemented
return None, None, None
def _get_serial_number(self):
return self.cert.get_serial_number()
def _get_all_extensions(self):
return pyopenssl_get_extensions_from_cert(self.cert)
def _get_ocsp_uri(self):
for i in range(self.cert.get_extension_count()):
ext = self.cert.get_extension(i)
if ext.get_short_name() == b'authorityInfoAccess':
v = str(ext)
m = re.search('^OCSP - URI:(.*)$', v, flags=re.MULTILINE)
if m:
return m.group(1)
return None
def get_certificate_info(module, backend, content, prefer_one_fingerprint=False):
if backend == 'cryptography':
info = CertificateInfoRetrievalCryptography(module, content)
elif backend == 'pyopenssl':
info = CertificateInfoRetrievalPyOpenSSL(module, content)
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
@@ -529,34 +375,17 @@ def select_backend(module, backend, content):
if backend == 'auto':
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
# First try cryptography, then pyOpenSSL
# Try cryptography
if can_use_cryptography:
backend = 'cryptography'
elif can_use_pyopenssl:
backend = 'pyopenssl'
# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION))
module.fail_json(msg=("Cannot detect any of the required Python libraries "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == 'pyopenssl':
if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR)
try:
getattr(crypto.X509Req, 'get_extensions')
except AttributeError:
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.0.0', collection_name='community.crypto')
return backend, CertificateInfoRetrievalPyOpenSSL(module, content)
elif backend == 'cryptography':
if backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)

View File

@@ -10,10 +10,9 @@ __metaclass__ = type
import os
from distutils.version import LooseVersion
from random import randrange
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLBadPassphraseError,
@@ -27,8 +26,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
cryptography_compare_public_keys,
cryptography_key_needs_digest_for_signing,
cryptography_serial_number_of_cert,
cryptography_verify_certificate_signature,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
@@ -38,11 +39,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
CertificateProvider,
)
try:
from OpenSSL import crypto
except ImportError:
pass
try:
import cryptography
from cryptography import x509
@@ -106,6 +102,9 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
except OpenSSLBadPassphraseError as exc:
module.fail_json(msg=str(exc))
if not cryptography_compare_public_keys(self.ca_cert.public_key(), self.ca_private_key.public_key()):
raise CertificateError('The CA private key does not belong to the CA certificate')
if cryptography_key_needs_digest_for_signing(self.ca_private_key):
if self.digest is None:
raise CertificateError(
@@ -169,7 +168,17 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
return self.cert.public_bytes(Encoding.PEM)
def needs_regeneration(self):
if super(OwnCACertificateBackendCryptography, self).needs_regeneration():
if super(OwnCACertificateBackendCryptography, self).needs_regeneration(not_before=self.notBefore, not_after=self.notAfter):
return True
self._ensure_existing_certificate_loaded()
# Check whether certificate is signed by CA certificate
if not cryptography_verify_certificate_signature(self.existing_certificate, self.ca_cert.public_key()):
return True
# Check subject
if self.ca_cert.subject != self.existing_certificate.issuer:
return True
# Check AuthorityKeyIdentifier
@@ -184,7 +193,6 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
except cryptography.x509.ExtensionNotFound:
expected_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key())
self._ensure_existing_certificate_loaded()
try:
ext = self.existing_certificate.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
if ext.value != expected_ext:
@@ -227,100 +235,6 @@ def generate_serial_number():
return result
class OwnCACertificateBackendPyOpenSSL(CertificateBackend):
def __init__(self, module):
super(OwnCACertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
self.notBefore = get_relative_time_option(self.module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend)
self.notAfter = get_relative_time_option(self.module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend)
self.digest = self.module.params['ownca_digest']
self.version = self.module.params['ownca_version']
self.serial_number = generate_serial_number()
if self.module.params['ownca_create_subject_key_identifier'] != 'create_if_not_provided':
self.module.fail_json(msg='ownca_create_subject_key_identifier cannot be used with the pyOpenSSL backend!')
if self.module.params['ownca_create_authority_key_identifier']:
self.module.warn('ownca_create_authority_key_identifier is ignored by the pyOpenSSL backend!')
self.ca_cert_path = self.module.params['ownca_path']
self.ca_cert_content = self.module.params['ownca_content']
if self.ca_cert_content is not None:
self.ca_cert_content = self.ca_cert_content.encode('utf-8')
self.ca_privatekey_path = self.module.params['ownca_privatekey_path']
self.ca_privatekey_content = self.module.params['ownca_privatekey_content']
if self.ca_privatekey_content is not None:
self.ca_privatekey_content = self.ca_privatekey_content.encode('utf-8')
self.ca_privatekey_passphrase = self.module.params['ownca_privatekey_passphrase']
if self.csr_content is None and not os.path.exists(self.csr_path):
raise CertificateError(
'The certificate signing request file {0} does not exist'.format(self.csr_path)
)
if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path):
raise CertificateError(
'The CA certificate file {0} does not exist'.format(self.ca_cert_path)
)
if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path):
raise CertificateError(
'The CA private key file {0} does not exist'.format(self.ca_privatekey_path)
)
self._ensure_csr_loaded()
self.ca_cert = load_certificate(
path=self.ca_cert_path,
content=self.ca_cert_content,
)
try:
self.ca_privatekey = load_privatekey(
path=self.ca_privatekey_path,
content=self.ca_privatekey_content,
passphrase=self.ca_privatekey_passphrase
)
except OpenSSLBadPassphraseError as exc:
self.module.fail_json(msg=str(exc))
def generate_certificate(self):
"""(Re-)Generate certificate."""
cert = crypto.X509()
cert.set_serial_number(self.serial_number)
cert.set_notBefore(to_bytes(self.notBefore))
cert.set_notAfter(to_bytes(self.notAfter))
cert.set_subject(self.csr.get_subject())
cert.set_issuer(self.ca_cert.get_subject())
cert.set_version(self.version - 1)
cert.set_pubkey(self.csr.get_pubkey())
cert.add_extensions(self.csr.get_extensions())
cert.sign(self.ca_privatekey, self.digest)
self.cert = cert
def get_certificate_data(self):
"""Return bytes for self.cert."""
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
def dump(self, include_certificate):
result = super(OwnCACertificateBackendPyOpenSSL, self).dump(include_certificate)
result.update({
'ca_cert': self.ca_cert_path,
'ca_privatekey': self.ca_privatekey_path,
})
if self.module.check_mode:
result.update({
'notBefore': self.notBefore,
'notAfter': self.notAfter,
'serial_number': self.serial_number,
})
else:
if self.cert is None:
self.cert = self.existing_certificate
result.update({
'notBefore': self.cert.get_notBefore(),
'notAfter': self.cert.get_notAfter(),
'serial_number': self.cert.get_serial_number(),
})
return result
class OwnCACertificateProvider(CertificateProvider):
def validate_module_args(self, module):
if module.params['ownca_path'] is None and module.params['ownca_content'] is None:
@@ -334,8 +248,6 @@ class OwnCACertificateProvider(CertificateProvider):
def create_backend(self, module, backend):
if backend == 'cryptography':
return OwnCACertificateBackendCryptography(module)
if backend == 'pyopenssl':
return OwnCACertificateBackendPyOpenSSL(module)
def add_ownca_provider_to_argument_spec(argument_spec):

View File

@@ -12,8 +12,6 @@ import os
from random import randrange
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
get_relative_time_option,
select_message_digest,
@@ -22,6 +20,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
cryptography_key_needs_digest_for_signing,
cryptography_serial_number_of_cert,
cryptography_verify_certificate_signature,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
@@ -30,11 +29,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
CertificateProvider,
)
try:
from OpenSSL import crypto
except ImportError:
pass
try:
import cryptography
from cryptography import x509
@@ -134,6 +128,18 @@ class SelfSignedCertificateBackendCryptography(CertificateBackend):
"""Return bytes for self.cert."""
return self.cert.public_bytes(Encoding.PEM)
def needs_regeneration(self):
if super(SelfSignedCertificateBackendCryptography, self).needs_regeneration(not_before=self.notBefore, not_after=self.notAfter):
return True
self._ensure_existing_certificate_loaded()
# Check whether certificate is signed by private key
if not cryptography_verify_certificate_signature(self.existing_certificate, self.privatekey.public_key()):
return True
return False
def dump(self, include_certificate):
result = super(SelfSignedCertificateBackendCryptography, self).dump(include_certificate)
@@ -163,76 +169,6 @@ def generate_serial_number():
return result
class SelfSignedCertificateBackendPyOpenSSL(CertificateBackend):
def __init__(self, module):
super(SelfSignedCertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
if module.params['selfsigned_create_subject_key_identifier'] != 'create_if_not_provided':
module.fail_json(msg='selfsigned_create_subject_key_identifier cannot be used with the pyOpenSSL backend!')
self.notBefore = get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend)
self.notAfter = get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend)
self.digest = module.params['selfsigned_digest']
self.version = module.params['selfsigned_version']
self.serial_number = generate_serial_number()
if self.csr_path is not None and not os.path.exists(self.csr_path):
raise CertificateError(
'The certificate signing request file {0} does not exist'.format(self.csr_path)
)
if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
raise CertificateError(
'The private key file {0} does not exist'.format(self.privatekey_path)
)
self._ensure_private_key_loaded()
self._ensure_csr_loaded()
if self.csr is None:
# Create empty CSR on the fly
self.csr = crypto.X509Req()
self.csr.set_pubkey(self.privatekey)
self.csr.sign(self.privatekey, self.digest)
def generate_certificate(self):
"""(Re-)Generate certificate."""
cert = crypto.X509()
cert.set_serial_number(self.serial_number)
cert.set_notBefore(to_bytes(self.notBefore))
cert.set_notAfter(to_bytes(self.notAfter))
cert.set_subject(self.csr.get_subject())
cert.set_issuer(self.csr.get_subject())
cert.set_version(self.version - 1)
cert.set_pubkey(self.csr.get_pubkey())
cert.add_extensions(self.csr.get_extensions())
cert.sign(self.privatekey, self.digest)
self.cert = cert
def get_certificate_data(self):
"""Return bytes for self.cert."""
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
def dump(self, include_certificate):
result = super(SelfSignedCertificateBackendPyOpenSSL, self).dump(include_certificate)
if self.module.check_mode:
result.update({
'notBefore': self.notBefore,
'notAfter': self.notAfter,
'serial_number': self.serial_number,
})
else:
if self.cert is None:
self.cert = self.existing_certificate
result.update({
'notBefore': self.cert.get_notBefore(),
'notAfter': self.cert.get_notAfter(),
'serial_number': self.cert.get_serial_number(),
})
return result
class SelfSignedCertificateProvider(CertificateProvider):
def validate_module_args(self, module):
if module.params['privatekey_path'] is None and module.params['privatekey_content'] is None:
@@ -244,8 +180,6 @@ class SelfSignedCertificateProvider(CertificateProvider):
def create_backend(self, module, backend):
if backend == 'cryptography':
return SelfSignedCertificateBackendCryptography(module)
if backend == 'pyopenssl':
return SelfSignedCertificateBackendPyOpenSSL(module)
def add_selfsigned_provider_to_argument_spec(argument_spec):

View File

@@ -9,10 +9,10 @@ __metaclass__ = type
import traceback
from distutils.version import LooseVersion
from ansible.module_utils.basic import missing_required_lib
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
cryptography_oid_to_name,
)
@@ -51,6 +51,7 @@ class CRLInfoRetrieval(object):
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):
self.crl_pem = identify_pem_format(self.content)
@@ -86,7 +87,7 @@ class CRLInfoRetrieval(object):
result['revoked_certificates'] = []
for cert in self.crl:
entry = cryptography_decode_revoked_certificate(cert)
result['revoked_certificates'].append(cryptography_dump_revoked(entry))
result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding))
return result

View File

@@ -12,11 +12,11 @@ import abc
import binascii
import traceback
from distutils.version import LooseVersion
from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
@@ -27,6 +27,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
load_privatekey,
load_certificate_request,
parse_name_field,
parse_ordered_name_field,
select_message_digest,
)
@@ -43,11 +44,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
REVOCATION_REASON_MAP,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
pyopenssl_normalize_name_attribute,
pyopenssl_parse_name_constraints,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import (
get_csr_info,
)
@@ -55,28 +51,8 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
MINIMAL_PYOPENSSL_VERSION = '0.15'
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
except ImportError:
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
PYOPENSSL_FOUND = True
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
# OpenSSL 1.1.0 or newer
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
else:
# OpenSSL 1.0.x or older
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
@@ -144,6 +120,7 @@ class CertificateSigningRequestBackend(object):
if self.create_subject_key_identifier and self.subject_key_identifier is not None:
module.fail_json(msg='subject_key_identifier cannot be specified if create_subject_key_identifier is true')
self.ordered_subject = False
self.subject = [
('C', module.params['country_name']),
('ST', module.params['state_or_province_name']),
@@ -153,11 +130,19 @@ class CertificateSigningRequestBackend(object):
('CN', module.params['common_name']),
('emailAddress', module.params['email_address']),
]
if module.params['subject']:
self.subject = self.subject + parse_name_field(module.params['subject'])
self.subject = [(entry[0], entry[1]) for entry in self.subject if entry[1]]
try:
if module.params['subject']:
self.subject = self.subject + parse_name_field(module.params['subject'], 'subject')
if module.params['subject_ordered']:
if self.subject:
raise CertificateSigningRequestError('subject_ordered cannot be combined with any other subject field')
self.subject = parse_ordered_name_field(module.params['subject_ordered'], 'subject_ordered')
self.ordered_subject = True
except ValueError as exc:
raise CertificateSigningRequestError(to_native(exc))
self.using_common_name_for_san = False
if not self.subjectAltName and module.params['use_common_name_for_san']:
for sub in self.subject:
@@ -273,174 +258,6 @@ class CertificateSigningRequestBackend(object):
return result
# Implementation with using pyOpenSSL
class CertificateSigningRequestPyOpenSSLBackend(CertificateSigningRequestBackend):
def __init__(self, module):
for o in ('create_subject_key_identifier', ):
if module.params[o]:
module.fail_json(msg='You cannot use {0} with the pyOpenSSL backend!'.format(o))
for o in ('subject_key_identifier', 'authority_key_identifier', 'authority_cert_issuer', 'authority_cert_serial_number', 'crl_distribution_points'):
if module.params[o] is not None:
module.fail_json(msg='You cannot use {0} with the pyOpenSSL backend!'.format(o))
super(CertificateSigningRequestPyOpenSSLBackend, self).__init__(module, 'pyopenssl')
def generate_csr(self):
"""(Re-)Generate CSR."""
self._ensure_private_key_loaded()
req = crypto.X509Req()
req.set_version(self.version - 1)
subject = req.get_subject()
for entry in self.subject:
if entry[1] is not None:
# Workaround for https://github.com/pyca/pyopenssl/issues/165
nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(entry[0]))
if nid == 0:
raise CertificateSigningRequestError('Unknown subject field identifier "{0}"'.format(entry[0]))
res = OpenSSL._util.lib.X509_NAME_add_entry_by_NID(subject._name, nid, OpenSSL._util.lib.MBSTRING_UTF8, to_bytes(entry[1]), -1, -1, 0)
if res == 0:
raise CertificateSigningRequestError('Invalid value for subject field identifier "{0}": {1}'.format(entry[0], entry[1]))
extensions = []
if self.subjectAltName:
altnames = ', '.join(self.subjectAltName)
try:
extensions.append(crypto.X509Extension(b"subjectAltName", self.subjectAltName_critical, altnames.encode('ascii')))
except OpenSSL.crypto.Error as e:
raise CertificateSigningRequestError(
'Error while parsing Subject Alternative Names {0} (check for missing type prefix, such as "DNS:"!): {1}'.format(
', '.join(["{0}".format(san) for san in self.subjectAltName]), str(e)
)
)
if self.keyUsage:
usages = ', '.join(self.keyUsage)
extensions.append(crypto.X509Extension(b"keyUsage", self.keyUsage_critical, usages.encode('ascii')))
if self.extendedKeyUsage:
usages = ', '.join(self.extendedKeyUsage)
extensions.append(crypto.X509Extension(b"extendedKeyUsage", self.extendedKeyUsage_critical, usages.encode('ascii')))
if self.basicConstraints:
usages = ', '.join(self.basicConstraints)
extensions.append(crypto.X509Extension(b"basicConstraints", self.basicConstraints_critical, usages.encode('ascii')))
if self.name_constraints_permitted or self.name_constraints_excluded:
usages = ', '.join(
['permitted;{0}'.format(name) for name in self.name_constraints_permitted] +
['excluded;{0}'.format(name) for name in self.name_constraints_excluded]
)
extensions.append(crypto.X509Extension(b"nameConstraints", self.name_constraints_critical, usages.encode('ascii')))
if self.ocspMustStaple:
extensions.append(crypto.X509Extension(OPENSSL_MUST_STAPLE_NAME, self.ocspMustStaple_critical, OPENSSL_MUST_STAPLE_VALUE))
if extensions:
req.add_extensions(extensions)
req.set_pubkey(self.privatekey)
req.sign(self.privatekey, self.digest)
self.csr = req
def get_csr_data(self):
"""Return bytes for self.csr."""
return crypto.dump_certificate_request(crypto.FILETYPE_PEM, self.csr)
def _check_csr(self):
def _check_subject(csr):
subject = [(OpenSSL._util.lib.OBJ_txt2nid(to_bytes(sub[0])), to_bytes(sub[1])) for sub in self.subject]
current_subject = [(OpenSSL._util.lib.OBJ_txt2nid(to_bytes(sub[0])), to_bytes(sub[1])) for sub in csr.get_subject().get_components()]
if not set(subject) == set(current_subject):
return False
return True
def _check_subjectAltName(extensions):
altnames_ext = next((ext for ext in extensions if ext.get_short_name() == b'subjectAltName'), '')
altnames = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
to_text(altnames_ext, errors='surrogate_or_strict').split(',') if altname.strip()]
if self.subjectAltName:
if (set(altnames) != set([pyopenssl_normalize_name_attribute(to_text(name)) for name in self.subjectAltName]) or
altnames_ext.get_critical() != self.subjectAltName_critical):
return False
else:
if altnames:
return False
return True
def _check_keyUsage_(extensions, extName, expected, critical):
usages_ext = [ext for ext in extensions if ext.get_short_name() == extName]
if (not usages_ext and expected) or (usages_ext and not expected):
return False
elif not usages_ext and not expected:
return True
else:
current = [OpenSSL._util.lib.OBJ_txt2nid(to_bytes(usage.strip())) for usage in str(usages_ext[0]).split(',')]
expected = [OpenSSL._util.lib.OBJ_txt2nid(to_bytes(usage)) for usage in expected]
return set(current) == set(expected) and usages_ext[0].get_critical() == critical
def _check_keyUsage(extensions):
usages_ext = [ext for ext in extensions if ext.get_short_name() == b'keyUsage']
if (not usages_ext and self.keyUsage) or (usages_ext and not self.keyUsage):
return False
elif not usages_ext and not self.keyUsage:
return True
else:
# OpenSSL._util.lib.OBJ_txt2nid() always returns 0 for all keyUsage values
# (since keyUsage has a fixed bitfield for these values and is not extensible).
# Therefore, we create an extension for the wanted values, and compare the
# data of the extensions (which is the serialized bitfield).
expected_ext = crypto.X509Extension(b"keyUsage", False, ', '.join(self.keyUsage).encode('ascii'))
return usages_ext[0].get_data() == expected_ext.get_data() and usages_ext[0].get_critical() == self.keyUsage_critical
def _check_extenededKeyUsage(extensions):
return _check_keyUsage_(extensions, b'extendedKeyUsage', self.extendedKeyUsage, self.extendedKeyUsage_critical)
def _check_basicConstraints(extensions):
return _check_keyUsage_(extensions, b'basicConstraints', self.basicConstraints, self.basicConstraints_critical)
def _check_nameConstraints(extensions):
nc_ext = next((ext for ext in extensions if ext.get_short_name() == b'nameConstraints'), '')
permitted, excluded = pyopenssl_parse_name_constraints(nc_ext)
if self.name_constraints_permitted or self.name_constraints_excluded:
if set(permitted) != set([pyopenssl_normalize_name_attribute(to_text(name)) for name in self.name_constraints_permitted]):
return False
if set(excluded) != set([pyopenssl_normalize_name_attribute(to_text(name)) for name in self.name_constraints_excluded]):
return False
if nc_ext.get_critical() != self.name_constraints_critical:
return False
else:
if permitted or excluded:
return False
return True
def _check_ocspMustStaple(extensions):
oms_ext = [ext for ext in extensions if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE]
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
# Older versions of libssl don't know about OCSP Must Staple
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
if self.ocspMustStaple:
return len(oms_ext) > 0 and oms_ext[0].get_critical() == self.ocspMustStaple_critical
else:
return len(oms_ext) == 0
def _check_extensions(csr):
extensions = csr.get_extensions()
return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and
_check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and
_check_ocspMustStaple(extensions) and _check_nameConstraints(extensions))
def _check_signature(csr):
try:
return csr.verify(self.privatekey)
except crypto.Error:
return False
return _check_subject(self.existing_csr) and _check_extensions(self.existing_csr) and _check_signature(self.existing_csr)
def parse_crl_distribution_points(module, crl_distribution_points):
result = []
for index, parse_crl_distribution_point in enumerate(crl_distribution_points):
@@ -594,7 +411,10 @@ class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBack
def _check_subject(csr):
subject = [(cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.subject]
current_subject = [(sub.oid, sub.value) for sub in csr.subject]
return set(subject) == set(current_subject)
if self.ordered_subject:
return subject == current_subject
else:
return set(subject) == set(current_subject)
def _find_extension(extensions, exttype):
return next(
@@ -754,42 +574,20 @@ class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBack
def select_backend(module, backend):
if module.params['version'] != 1:
module.deprecate('The version option will only support allowed values from community.crypto 2.0.0 on. '
'Currently, only the value 1 is allowed by RFC 2986',
version='2.0.0', collection_name='community.crypto')
if backend == 'auto':
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
# First try cryptography, then pyOpenSSL
# Try cryptography
if can_use_cryptography:
backend = 'cryptography'
elif can_use_pyopenssl:
backend = 'pyopenssl'
# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION))
module.fail_json(msg=("Cannot detect any of the required Python libraries "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == 'pyopenssl':
if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR)
try:
getattr(crypto.X509Req, 'get_extensions')
except AttributeError:
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.0.0', collection_name='community.crypto')
return backend, CertificateSigningRequestPyOpenSSLBackend(module)
elif backend == 'cryptography':
if backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
@@ -805,8 +603,9 @@ def get_csr_argument_spec():
privatekey_path=dict(type='path'),
privatekey_content=dict(type='str', no_log=True),
privatekey_passphrase=dict(type='str', no_log=True),
version=dict(type='int', default=1),
version=dict(type='int', default=1, choices=[1]),
subject=dict(type='dict'),
subject_ordered=dict(type='list', elements='dict'),
country_name=dict(type='str', aliases=['C', 'countryName']),
state_or_province_name=dict(type='str', aliases=['ST', 'stateOrProvinceName']),
locality_name=dict(type='str', aliases=['L', 'localityName']),
@@ -853,13 +652,14 @@ def get_csr_argument_spec():
),
mutually_exclusive=[('full_name', 'relative_name')]
),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
),
required_together=[
['authority_cert_issuer', 'authority_cert_serial_number'],
],
mutually_exclusive=[
['privatekey_path', 'privatekey_content'],
['subject', 'subject_ordered'],
],
required_one_of=[
['privatekey_path', 'privatekey_content'],

View File

@@ -13,15 +13,14 @@ import abc
import binascii
import traceback
from distutils.version import LooseVersion
from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_certificate_request,
get_fingerprint_of_bytes,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
@@ -30,38 +29,11 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
cryptography_oid_to_name,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import (
pyopenssl_get_extensions_from_csr,
pyopenssl_normalize_name,
pyopenssl_normalize_name_attribute,
pyopenssl_parse_name_constraints,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
get_publickey_info,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
MINIMAL_PYOPENSSL_VERSION = '0.15'
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
# OpenSSL 1.1.0 or newer
OPENSSL_MUST_STAPLE_NAME = b"tlsfeature"
OPENSSL_MUST_STAPLE_VALUE = b"status_request"
else:
# OpenSSL 1.0.x or older
OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24"
OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05"
except ImportError:
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
PYOPENSSL_FOUND = True
CRYPTOGRAPHY_IMP_ERR = None
try:
@@ -173,20 +145,19 @@ class CSRInfoRetrieval(object):
'public_key_fingerprints': public_key_info['fingerprints'],
})
if self.backend != 'pyopenssl':
ski = self._get_subject_key_identifier()
if ski is not None:
ski = to_native(binascii.hexlify(ski))
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
result['subject_key_identifier'] = ski
ski = self._get_subject_key_identifier()
if ski is not None:
ski = to_native(binascii.hexlify(ski))
ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
result['subject_key_identifier'] = ski
aki, aci, acsn = self._get_authority_key_identifier()
if aki is not None:
aki = to_native(binascii.hexlify(aki))
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
result['authority_key_identifier'] = aki
result['authority_cert_issuer'] = aci
result['authority_cert_serial_number'] = acsn
aki, aci, acsn = self._get_authority_key_identifier()
if aki is not None:
aki = to_native(binascii.hexlify(aki))
aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
result['authority_key_identifier'] = aki
result['authority_cert_issuer'] = aci
result['authority_cert_serial_number'] = acsn
result['extensions_by_oid'] = self._get_all_extensions()
@@ -203,6 +174,7 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
"""Validate the supplied CSR, using the cryptography backend"""
def __init__(self, module, content, validate_signature):
super(CSRInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, validate_signature)
self.name_encoding = module.params.get('name_encoding', 'ignore')
def _get_subject_ordered(self):
result = []
@@ -285,7 +257,7 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
def _get_subject_alt_name(self):
try:
san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName)
result = [cryptography_decode_name(san) for san in san_ext.value]
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
@@ -293,8 +265,8 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
def _get_name_constraints(self):
try:
nc_ext = self.csr.extensions.get_extension_for_class(x509.NameConstraints)
permitted = [cryptography_decode_name(san) for san in nc_ext.value.permitted_subtrees or []]
excluded = [cryptography_decode_name(san) for san in nc_ext.value.excluded_subtrees or []]
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
@@ -320,7 +292,7 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
issuer = None
if ext.value.authority_cert_issuer is not None:
issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
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
@@ -332,112 +304,9 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
return self.csr.is_signature_valid
class CSRInfoRetrievalPyOpenSSL(CSRInfoRetrieval):
"""validate the supplied CSR."""
def __init__(self, module, content, validate_signature):
super(CSRInfoRetrievalPyOpenSSL, self).__init__(module, 'pyopenssl', content, validate_signature)
def __get_name(self, name):
result = []
for sub in name.get_components():
result.append([pyopenssl_normalize_name(sub[0]), to_text(sub[1])])
return result
def _get_subject_ordered(self):
return self.__get_name(self.csr.get_subject())
def _get_extension(self, short_name):
for extension in self.csr.get_extensions():
if extension.get_short_name() == short_name:
result = [
pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',')
]
return sorted(result), bool(extension.get_critical())
return None, False
def _get_key_usage(self):
return self._get_extension(b'keyUsage')
def _get_extended_key_usage(self):
return self._get_extension(b'extendedKeyUsage')
def _get_basic_constraints(self):
return self._get_extension(b'basicConstraints')
def _get_ocsp_must_staple(self):
extensions = self.csr.get_extensions()
oms_ext = [
ext for ext in extensions
if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE
]
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000:
# Older versions of libssl don't know about OCSP Must Staple
oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05'])
if oms_ext:
return True, bool(oms_ext[0].get_critical())
else:
return None, False
def _get_subject_alt_name(self):
for extension in self.csr.get_extensions():
if extension.get_short_name() == b'subjectAltName':
result = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in
to_text(extension, errors='surrogate_or_strict').split(', ')]
return result, bool(extension.get_critical())
return None, False
def _get_name_constraints(self):
for extension in self.csr.get_extensions():
if extension.get_short_name() == b'nameConstraints':
permitted, excluded = pyopenssl_parse_name_constraints(extension)
return permitted, excluded, bool(extension.get_critical())
return None, None, False
def _get_public_key_pem(self):
try:
return crypto.dump_publickey(
crypto.FILETYPE_PEM,
self.csr.get_pubkey(),
)
except AttributeError:
try:
bio = crypto._new_mem_buf()
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.csr.get_pubkey()._pkey)
if rc != 1:
crypto._raise_current_error()
return crypto._bio_to_string(bio)
except AttributeError:
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
def _get_public_key_object(self):
return self.csr.get_pubkey()
def _get_subject_key_identifier(self):
# Won't be implemented
return None
def _get_authority_key_identifier(self):
# Won't be implemented
return None, None, None
def _get_all_extensions(self):
return pyopenssl_get_extensions_from_csr(self.csr)
def _is_signature_valid(self):
try:
return bool(self.csr.verify(self.csr.get_pubkey()))
except crypto.Error:
# OpenSSL error means that key is not consistent
return False
def get_csr_info(module, backend, content, validate_signature=True, prefer_one_fingerprint=False):
if backend == 'cryptography':
info = CSRInfoRetrievalCryptography(module, content, validate_signature=validate_signature)
elif backend == 'pyopenssl':
info = CSRInfoRetrievalPyOpenSSL(module, content, validate_signature=validate_signature)
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
@@ -445,34 +314,17 @@ def select_backend(module, backend, content, validate_signature=True):
if backend == 'auto':
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
# First try cryptography, then pyOpenSSL
# Try cryptography
if can_use_cryptography:
backend = 'cryptography'
elif can_use_pyopenssl:
backend = 'pyopenssl'
# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION))
module.fail_json(msg=("Cannot detect the required Python library "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == 'pyopenssl':
if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR)
try:
getattr(crypto.X509Req, 'get_extensions')
except AttributeError:
module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs')
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.0.0', collection_name='community.crypto')
return backend, CSRInfoRetrievalPyOpenSSL(module, content, validate_signature=validate_signature)
elif backend == 'cryptography':
if backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)

View File

@@ -12,12 +12,12 @@ import abc
import base64
import traceback
from distutils.version import LooseVersion
from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
CRYPTOGRAPHY_HAS_X25519,
CRYPTOGRAPHY_HAS_X25519_FULL,
@@ -25,11 +25,9 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.basic impo
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_ED448,
OpenSSLObjectError,
OpenSSLBadPassphraseError,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_privatekey,
get_fingerprint_of_privatekey,
)
@@ -46,20 +44,8 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
MINIMAL_PYOPENSSL_VERSION = '0.6'
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
except ImportError:
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
PYOPENSSL_FOUND = True
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
@@ -263,61 +249,6 @@ class PrivateKeyBackend:
return result
# Implementation with using pyOpenSSL
class PrivateKeyPyOpenSSLBackend(PrivateKeyBackend):
def __init__(self, module):
super(PrivateKeyPyOpenSSLBackend, self).__init__(module=module, backend='pyopenssl')
if self.type == 'RSA':
self.openssl_type = crypto.TYPE_RSA
elif self.type == 'DSA':
self.openssl_type = crypto.TYPE_DSA
else:
self.module.fail_json(msg="PyOpenSSL backend only supports RSA and DSA keys.")
if self.format != 'auto_ignore':
self.module.fail_json(msg="PyOpenSSL backend only supports auto_ignore format.")
def generate_private_key(self):
"""(Re-)Generate private key."""
self.private_key = crypto.PKey()
try:
self.private_key.generate_key(self.openssl_type, self.size)
except (TypeError, ValueError) as exc:
raise PrivateKeyError(exc)
def _ensure_existing_private_key_loaded(self):
if self.existing_private_key is None and self.has_existing():
try:
self.existing_private_key = load_privatekey(
None, self.passphrase, content=self.existing_private_key_bytes, backend=self.backend)
except OpenSSLBadPassphraseError as exc:
raise PrivateKeyError(exc)
def get_private_key_data(self):
"""Return bytes for self.private_key"""
if self.cipher and self.passphrase:
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.private_key,
self.cipher, to_bytes(self.passphrase))
else:
return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.private_key)
def _check_passphrase(self):
try:
load_privatekey(None, self.passphrase, content=self.existing_private_key_bytes, backend=self.backend)
return True
except Exception as dummy:
return False
def _check_size_and_type(self):
return self.size == self.existing_private_key.bits() and self.openssl_type == self.existing_private_key.type()
def _check_format(self):
# Not supported by this backend
return True
# Implementation with using cryptography
class PrivateKeyCryptographyBackend(PrivateKeyBackend):
@@ -550,36 +481,16 @@ def select_backend(module, backend):
if backend == 'auto':
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
# Decision
if module.params['cipher'] and module.params['passphrase'] and module.params['cipher'] != 'auto':
# First try pyOpenSSL, then cryptography
if can_use_pyopenssl:
backend = 'pyopenssl'
elif can_use_cryptography:
backend = 'cryptography'
else:
# First try cryptography, then pyOpenSSL
if can_use_cryptography:
backend = 'cryptography'
elif can_use_pyopenssl:
backend = 'pyopenssl'
if can_use_cryptography:
backend = 'cryptography'
# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION))
if backend == 'pyopenssl':
if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR)
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.0.0', collection_name='community.crypto')
return backend, PrivateKeyPyOpenSSLBackend(module)
elif backend == 'cryptography':
module.fail_json(msg=("Cannot detect the required Python library "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
@@ -605,7 +516,7 @@ def get_privatekey_argument_spec():
cipher=dict(type='str'),
format=dict(type='str', default='auto_ignore', choices=['pkcs1', 'pkcs8', 'raw', 'auto', 'auto_ignore']),
format_mismatch=dict(type='str', default='regenerate', choices=['regenerate', 'convert']),
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
regenerate=dict(
type='str',
default='full_idempotence',

View File

@@ -0,0 +1,235 @@
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2022, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import abc
import traceback
from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.io import (
load_file,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
CRYPTOGRAPHY_HAS_X25519,
CRYPTOGRAPHY_HAS_X448,
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_ED448,
OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.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.crypto.module_backends.common import ArgumentSpec
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
import cryptography.exceptions
import cryptography.hazmat.backends
import cryptography.hazmat.primitives.serialization
import cryptography.hazmat.primitives.asymmetric.rsa
import cryptography.hazmat.primitives.asymmetric.dsa
import cryptography.hazmat.primitives.asymmetric.ec
import cryptography.hazmat.primitives.asymmetric.utils
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
CRYPTOGRAPHY_FOUND = False
else:
CRYPTOGRAPHY_FOUND = True
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)
@six.add_metaclass(abc.ABCMeta)
class PrivateKeyConvertBackend:
def __init__(self, module, backend):
self.module = module
self.src_path = module.params['src_path']
self.src_content = module.params['src_content']
self.src_passphrase = module.params['src_passphrase']
self.format = module.params['format']
self.dest_passphrase = module.params['dest_passphrase']
self.backend = backend
self.src_private_key = None
if self.src_path is not None:
self.src_private_key_bytes = load_file(self.src_path, module)
else:
self.src_private_key_bytes = self.src_content.encode('utf-8')
self.dest_private_key = None
self.dest_private_key_bytes = None
@abc.abstractmethod
def get_private_key_data(self):
"""Return bytes for self.src_private_key in output format."""
pass
def set_existing_destination(self, privatekey_bytes):
"""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):
"""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, passphrase, current_hint=None):
"""Check whether data cna be loaded as a private key with the provided passphrase. Return tuple (type, private_key)."""
pass
def needs_conversion(self):
"""Check whether a conversion is necessary. Must only be called if needs_regeneration() returned False."""
dummy, self.src_private_key = self._load_private_key(self.src_private_key_bytes, self.src_passphrase)
if not self.has_existing_destination():
return True
try:
format, self.dest_private_key = self._load_private_key(self.dest_private_key_bytes, self.dest_passphrase, current_hint=self.src_private_key)
except Exception:
return True
return format != self.format or not cryptography_compare_private_keys(self.dest_private_key, self.src_private_key)
def dump(self):
"""Serialize the object into a dictionary."""
return {}
# Implementation with using cryptography
class PrivateKeyConvertCryptographyBackend(PrivateKeyConvertBackend):
def __init__(self, module):
super(PrivateKeyConvertCryptographyBackend, self).__init__(module=module, backend='cryptography')
self.cryptography_backend = cryptography.hazmat.backends.default_backend()
def get_private_key_data(self):
"""Return bytes for self.src_private_key in output format"""
# 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
except AttributeError:
self.module.fail_json(msg='Cryptography backend does not support the selected output format "{0}"'.format(self.format))
# Select key encryption
encryption_algorithm = 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 as dummy:
self.module.fail_json(
msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(self.format)
)
except Exception as dummy:
self.module.fail_json(
msg='Error while serializing the private key in the required format "{0}"'.format(self.format),
exception=traceback.format_exc()
)
def _load_private_key(self, data, passphrase, current_hint=None):
try:
# Interpret bytes depending on format.
format = identify_private_key_format(data)
if format == 'raw':
if passphrase is not None:
raise PrivateKeyError('Cannot load raw key with passphrase')
if len(data) == 56 and CRYPTOGRAPHY_HAS_X448:
return format, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data)
if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448:
return format, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data)
if len(data) == 32:
if CRYPTOGRAPHY_HAS_X25519 and not CRYPTOGRAPHY_HAS_ED25519:
return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
if CRYPTOGRAPHY_HAS_ED25519 and not CRYPTOGRAPHY_HAS_X25519:
return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519:
if isinstance(current_hint, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey):
try:
return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
except Exception:
return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
else:
try:
return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
except Exception:
return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
raise PrivateKeyError('Cannot load raw key')
else:
return format, cryptography.hazmat.primitives.serialization.load_pem_private_key(
data,
None if passphrase is None else to_bytes(passphrase),
backend=self.cryptography_backend
)
except Exception as e:
raise PrivateKeyError(e)
def select_backend(module):
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
return PrivateKeyConvertCryptographyBackend(module)
def get_privatekey_argument_spec():
return ArgumentSpec(
argument_spec=dict(
src_path=dict(type='path'),
src_content=dict(type='str'),
src_passphrase=dict(type='str', no_log=True),
dest_passphrase=dict(type='str', no_log=True),
format=dict(type='str', required=True, choices=['pkcs1', 'pkcs8', 'raw']),
),
mutually_exclusive=[
['src_path', 'src_content'],
],
required_one_of=[
['src_path', 'src_content'],
],
)

View File

@@ -12,12 +12,12 @@ __metaclass__ = type
import abc
import traceback
from distutils.version import LooseVersion
from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_native, to_bytes
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_ED448,
@@ -36,24 +36,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.math impor
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
_get_cryptography_public_key_info,
_bigint_to_int,
_get_pyopenssl_public_key_info,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
MINIMAL_PYOPENSSL_VERSION = '0.15'
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
except ImportError:
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
PYOPENSSL_FOUND = True
CRYPTOGRAPHY_IMP_ERR = None
try:
@@ -69,20 +55,21 @@ else:
SIGNATURE_TEST_DATA = b'1234'
def _get_cryptography_private_key_info(key):
def _get_cryptography_private_key_info(key, need_private_key_data=False):
key_type, key_public_data = _get_cryptography_public_key_info(key.public_key())
key_private_data = dict()
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
private_numbers = key.private_numbers()
key_private_data['p'] = private_numbers.p
key_private_data['q'] = private_numbers.q
key_private_data['exponent'] = private_numbers.d
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
private_numbers = key.private_numbers()
key_private_data['x'] = private_numbers.x
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
private_numbers = key.private_numbers()
key_private_data['multiplier'] = private_numbers.private_value
if need_private_key_data:
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
private_numbers = key.private_numbers()
key_private_data['p'] = private_numbers.p
key_private_data['q'] = private_numbers.q
key_private_data['exponent'] = private_numbers.d
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
private_numbers = key.private_numbers()
key_private_data['x'] = private_numbers.x
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
private_numbers = key.private_numbers()
key_private_data['multiplier'] = private_numbers.private_value
return key_type, key_public_data, key_private_data
@@ -188,20 +175,21 @@ class PrivateKeyParseError(OpenSSLObjectError):
@six.add_metaclass(abc.ABCMeta)
class PrivateKeyInfoRetrieval(object):
def __init__(self, module, backend, content, passphrase=None, return_private_key_data=False):
def __init__(self, module, backend, content, passphrase=None, return_private_key_data=False, check_consistency=False):
# content must be a bytes string
self.module = module
self.backend = backend
self.content = content
self.passphrase = passphrase
self.return_private_key_data = return_private_key_data
self.check_consistency = check_consistency
@abc.abstractmethod
def _get_public_key(self, binary):
pass
@abc.abstractmethod
def _get_key_info(self):
def _get_key_info(self, need_private_key_data=False):
pass
@abc.abstractmethod
@@ -230,20 +218,22 @@ class PrivateKeyInfoRetrieval(object):
result['public_key_fingerprints'] = get_fingerprint_of_bytes(
pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict()
key_type, key_public_data, key_private_data = self._get_key_info()
key_type, key_public_data, key_private_data = self._get_key_info(
need_private_key_data=self.return_private_key_data or self.check_consistency)
result['type'] = key_type
result['public_data'] = key_public_data
if self.return_private_key_data:
result['private_data'] = key_private_data
result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data)
if result['key_is_consistent'] is False:
# Only fail when it is False, to avoid to fail on None (which means "we don't know")
msg = (
"Private key is not consistent! (See "
"https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)"
)
raise PrivateKeyConsistencyError(msg, result)
if self.check_consistency:
result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data)
if result['key_is_consistent'] is False:
# Only fail when it is False, to avoid to fail on None (which means "we 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)
return result
@@ -258,177 +248,39 @@ class PrivateKeyInfoRetrievalCryptography(PrivateKeyInfoRetrieval):
serialization.PublicFormat.SubjectPublicKeyInfo
)
def _get_key_info(self):
return _get_cryptography_private_key_info(self.key)
def _get_key_info(self, need_private_key_data=False):
return _get_cryptography_private_key_info(self.key, need_private_key_data=need_private_key_data)
def _is_key_consistent(self, key_public_data, key_private_data):
return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data)
class PrivateKeyInfoRetrievalPyOpenSSL(PrivateKeyInfoRetrieval):
"""validate the supplied private key."""
def __init__(self, module, content, **kwargs):
super(PrivateKeyInfoRetrievalPyOpenSSL, self).__init__(module, 'pyopenssl', content, **kwargs)
def _get_public_key(self, binary):
try:
return crypto.dump_publickey(
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
self.key
)
except AttributeError:
try:
# pyOpenSSL < 16.0:
bio = crypto._new_mem_buf()
if binary:
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.key._pkey)
else:
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.key._pkey)
if rc != 1:
crypto._raise_current_error()
return crypto._bio_to_string(bio)
except AttributeError:
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
def _get_key_info(self):
key_type, key_public_data, try_fallback = _get_pyopenssl_public_key_info(self.key)
key_private_data = dict()
openssl_key_type = self.key.type()
if crypto.TYPE_RSA == openssl_key_type:
try:
# Use OpenSSL directly to extract key data
key = OpenSSL._util.lib.EVP_PKEY_get1_RSA(self.key._pkey)
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.RSA_free)
# OpenSSL 1.1 and newer have functions to extract the parameters
# from the EVP PKEY data structures. Older versions didn't have
# these getters, and it was common use to simply access the values
# directly. Since there's no guarantee that these data structures
# will still be accessible in the future, we use the getters for
# 1.1 and later, and directly access the values for 1.0.x and
# earlier.
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
# Get modulus and exponents
n = OpenSSL._util.ffi.new("BIGNUM **")
e = OpenSSL._util.ffi.new("BIGNUM **")
d = OpenSSL._util.ffi.new("BIGNUM **")
OpenSSL._util.lib.RSA_get0_key(key, n, e, d)
key_private_data['exponent'] = _bigint_to_int(d[0])
# Get factors
p = OpenSSL._util.ffi.new("BIGNUM **")
q = OpenSSL._util.ffi.new("BIGNUM **")
OpenSSL._util.lib.RSA_get0_factors(key, p, q)
key_private_data['p'] = _bigint_to_int(p[0])
key_private_data['q'] = _bigint_to_int(q[0])
else:
# Get private exponent
key_private_data['exponent'] = _bigint_to_int(key.d)
# Get factors
key_private_data['p'] = _bigint_to_int(key.p)
key_private_data['q'] = _bigint_to_int(key.q)
except AttributeError:
try_fallback = True
elif crypto.TYPE_DSA == openssl_key_type:
try:
# Use OpenSSL directly to extract key data
key = OpenSSL._util.lib.EVP_PKEY_get1_DSA(self.key._pkey)
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.DSA_free)
# OpenSSL 1.1 and newer have functions to extract the parameters
# from the EVP PKEY data structures. Older versions didn't have
# these getters, and it was common use to simply access the values
# directly. Since there's no guarantee that these data structures
# will still be accessible in the future, we use the getters for
# 1.1 and later, and directly access the values for 1.0.x and
# earlier.
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
# Get private key exponents
y = OpenSSL._util.ffi.new("BIGNUM **")
x = OpenSSL._util.ffi.new("BIGNUM **")
OpenSSL._util.lib.DSA_get0_key(key, y, x)
key_private_data['x'] = _bigint_to_int(x[0])
else:
# Get private key exponents
key_private_data['x'] = _bigint_to_int(key.priv_key)
except AttributeError:
try_fallback = True
else:
# Return 'unknown'
key_type = 'unknown ({0})'.format(self.key.type())
# If needed and if possible, fall back to cryptography
if try_fallback and PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
return _get_cryptography_private_key_info(self.key.to_cryptography_key())
return key_type, key_public_data, key_private_data
def _is_key_consistent(self, key_public_data, key_private_data):
openssl_key_type = self.key.type()
if crypto.TYPE_RSA == openssl_key_type:
try:
return self.key.check()
except crypto.Error:
# OpenSSL error means that key is not consistent
return False
if crypto.TYPE_DSA == openssl_key_type:
result = _check_dsa_consistency(key_public_data, key_private_data)
if result is not None:
return result
signature = crypto.sign(self.key, SIGNATURE_TEST_DATA, 'sha256')
# Verify wants a cert (where it can get the public key from)
cert = crypto.X509()
cert.set_pubkey(self.key)
try:
crypto.verify(cert, signature, SIGNATURE_TEST_DATA, 'sha256')
return True
except crypto.Error:
return False
# If needed and if possible, fall back to cryptography
if PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
return _is_cryptography_key_consistent(self.key.to_cryptography_key(), key_public_data, key_private_data)
return None
def get_privatekey_info(module, backend, content, passphrase=None, return_private_key_data=False, prefer_one_fingerprint=False):
if backend == 'cryptography':
info = PrivateKeyInfoRetrievalCryptography(
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
elif backend == 'pyopenssl':
info = PrivateKeyInfoRetrievalPyOpenSSL(
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
def select_backend(module, backend, content, passphrase=None, return_private_key_data=False):
def select_backend(module, backend, content, passphrase=None, return_private_key_data=False, check_consistency=False):
if backend == 'auto':
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
# First try cryptography, then pyOpenSSL
# Try cryptography
if can_use_cryptography:
backend = 'cryptography'
elif can_use_pyopenssl:
backend = 'pyopenssl'
# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION))
module.fail_json(msg=("Cannot detect the required Python library "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == 'pyopenssl':
if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR)
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.0.0', collection_name='community.crypto')
return backend, PrivateKeyInfoRetrievalPyOpenSSL(
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
elif backend == 'cryptography':
if backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
return backend, PrivateKeyInfoRetrievalCryptography(
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data, check_consistency=check_consistency)
else:
raise ValueError('Unsupported value for backend: {0}'.format(backend))

View File

@@ -10,12 +10,12 @@ __metaclass__ = type
import abc
import traceback
from distutils.version import LooseVersion
from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
CRYPTOGRAPHY_HAS_X25519,
CRYPTOGRAPHY_HAS_X448,
@@ -31,18 +31,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
MINIMAL_PYOPENSSL_VERSION = '16.0.0' # when working with public key objects, the minimal required version is 0.15
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
except ImportError:
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
PYOPENSSL_FOUND = True
CRYPTOGRAPHY_IMP_ERR = None
try:
@@ -93,98 +81,6 @@ def _get_cryptography_public_key_info(key):
return key_type, key_public_data
def _bigint_to_int(bn):
'''Convert OpenSSL BIGINT to Python integer'''
if bn == OpenSSL._util.ffi.NULL:
return None
hexstr = OpenSSL._util.lib.BN_bn2hex(bn)
try:
return int(OpenSSL._util.ffi.string(hexstr), 16)
finally:
OpenSSL._util.lib.OPENSSL_free(hexstr)
def _get_pyopenssl_public_key_info(key):
key_public_data = dict()
try_fallback = True
openssl_key_type = key.type()
if crypto.TYPE_RSA == openssl_key_type:
key_type = 'RSA'
key_public_data['size'] = key.bits()
try:
# Use OpenSSL directly to extract key data
key = OpenSSL._util.lib.EVP_PKEY_get1_RSA(key._pkey)
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.RSA_free)
# OpenSSL 1.1 and newer have functions to extract the parameters
# from the EVP PKEY data structures. Older versions didn't have
# these getters, and it was common use to simply access the values
# directly. Since there's no guarantee that these data structures
# will still be accessible in the future, we use the getters for
# 1.1 and later, and directly access the values for 1.0.x and
# earlier.
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
# Get modulus and exponents
n = OpenSSL._util.ffi.new("BIGNUM **")
e = OpenSSL._util.ffi.new("BIGNUM **")
d = OpenSSL._util.ffi.new("BIGNUM **")
OpenSSL._util.lib.RSA_get0_key(key, n, e, d)
key_public_data['modulus'] = _bigint_to_int(n[0])
key_public_data['exponent'] = _bigint_to_int(e[0])
else:
# Get modulus and exponents
key_public_data['modulus'] = _bigint_to_int(key.n)
key_public_data['exponent'] = _bigint_to_int(key.e)
try_fallback = False
except AttributeError:
# Use fallback if available
pass
elif crypto.TYPE_DSA == openssl_key_type:
key_type = 'DSA'
key_public_data['size'] = key.bits()
try:
# Use OpenSSL directly to extract key data
key = OpenSSL._util.lib.EVP_PKEY_get1_DSA(key._pkey)
key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.DSA_free)
# OpenSSL 1.1 and newer have functions to extract the parameters
# from the EVP PKEY data structures. Older versions didn't have
# these getters, and it was common use to simply access the values
# directly. Since there's no guarantee that these data structures
# will still be accessible in the future, we use the getters for
# 1.1 and later, and directly access the values for 1.0.x and
# earlier.
if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000:
# Get public parameters (primes and group element)
p = OpenSSL._util.ffi.new("BIGNUM **")
q = OpenSSL._util.ffi.new("BIGNUM **")
g = OpenSSL._util.ffi.new("BIGNUM **")
OpenSSL._util.lib.DSA_get0_pqg(key, p, q, g)
key_public_data['p'] = _bigint_to_int(p[0])
key_public_data['q'] = _bigint_to_int(q[0])
key_public_data['g'] = _bigint_to_int(g[0])
# Get public key exponents
y = OpenSSL._util.ffi.new("BIGNUM **")
x = OpenSSL._util.ffi.new("BIGNUM **")
OpenSSL._util.lib.DSA_get0_key(key, y, x)
key_public_data['y'] = _bigint_to_int(y[0])
else:
# Get public parameters (primes and group element)
key_public_data['p'] = _bigint_to_int(key.p)
key_public_data['q'] = _bigint_to_int(key.q)
key_public_data['g'] = _bigint_to_int(key.g)
# Get public key exponents
key_public_data['y'] = _bigint_to_int(key.pub_key)
try_fallback = False
except AttributeError:
# Use fallback if available
pass
else:
# Return 'unknown'
key_type = 'unknown ({0})'.format(key.type())
return key_type, key_public_data, try_fallback
class PublicKeyParseError(OpenSSLObjectError):
def __init__(self, msg, result):
super(PublicKeyParseError, self).__init__(msg)
@@ -242,46 +138,9 @@ class PublicKeyInfoRetrievalCryptography(PublicKeyInfoRetrieval):
return _get_cryptography_public_key_info(self.key)
class PublicKeyInfoRetrievalPyOpenSSL(PublicKeyInfoRetrieval):
"""validate the supplied public key."""
def __init__(self, module, content=None, key=None):
super(PublicKeyInfoRetrievalPyOpenSSL, self).__init__(module, 'pyopenssl', content=content, key=key)
def _get_public_key(self, binary):
try:
return crypto.dump_publickey(
crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM,
self.key
)
except AttributeError:
try:
# pyOpenSSL < 16.0:
bio = crypto._new_mem_buf()
if binary:
rc = crypto._lib.i2d_PUBKEY_bio(bio, self.key._pkey)
else:
rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.key._pkey)
if rc != 1:
crypto._raise_current_error()
return crypto._bio_to_string(bio)
except AttributeError:
self.module.warn('Your pyOpenSSL version does not support dumping public keys. '
'Please upgrade to version 16.0 or newer, or use the cryptography backend.')
def _get_key_info(self):
key_type, key_public_data, try_fallback = _get_pyopenssl_public_key_info(self.key)
# If needed and if possible, fall back to cryptography
if try_fallback and PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND:
return _get_cryptography_public_key_info(self.key.to_cryptography_key())
return key_type, key_public_data
def get_publickey_info(module, backend, content=None, key=None, prefer_one_fingerprint=False):
if backend == 'cryptography':
info = PublicKeyInfoRetrievalCryptography(module, content=content, key=key)
elif backend == 'pyopenssl':
info = PublicKeyInfoRetrievalPyOpenSSL(module, content=content, key=key)
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
@@ -289,29 +148,17 @@ def select_backend(module, backend, content=None, key=None):
if backend == 'auto':
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
# First try cryptography, then pyOpenSSL
# Try cryptography
if can_use_cryptography:
backend = 'cryptography'
elif can_use_pyopenssl:
backend = 'pyopenssl'
# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION))
module.fail_json(msg=("Cannot detect any of the required Python libraries "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == 'pyopenssl':
if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR)
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.0.0', collection_name='community.crypto')
return backend, PublicKeyInfoRetrievalPyOpenSSL(module, content=content, key=key)
elif backend == 'cryptography':
if backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)

View File

@@ -72,3 +72,13 @@ def split_pem_list(text, keep_inbetween=False):
result.append(''.join(current))
current = [] if keep_inbetween else None
return result
def extract_first_pem(text):
'''
Given one PEM or multiple concatenated PEM objects, return only the first one, or None if there is none.
'''
all_pems = split_pem_list(text)
if not all_pems:
return None
return all_pems[0]

View File

@@ -1,154 +0,0 @@
# -*- coding: utf-8 -*-
#
# (c) 2019, Felix Fontein <felix@fontein.de>
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import base64
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress
try:
import OpenSSL
except ImportError:
# Error handled in the calling module.
pass
from ._objects import (
NORMALIZE_NAMES_SHORT,
NORMALIZE_NAMES,
)
from ._obj2txt import obj2txt
from .basic import (
OpenSSLObjectError,
)
def pyopenssl_normalize_name(name, short=False):
nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(name))
if nid != 0:
b_name = OpenSSL._util.lib.OBJ_nid2ln(nid)
name = to_text(OpenSSL._util.ffi.string(b_name))
if short:
return NORMALIZE_NAMES_SHORT.get(name, name)
else:
return NORMALIZE_NAMES.get(name, name)
def pyopenssl_normalize_name_attribute(san):
# apparently openssl returns 'IP address' not 'IP' as specifier when converting the subjectAltName to string
# although it won't accept this specifier when generating the CSR. (https://github.com/openssl/openssl/issues/4004)
if san.startswith('IP Address:'):
san = 'IP:' + san[len('IP Address:'):]
if san.startswith('IP:'):
address = san[3:]
if '/' in address:
ip = compat_ipaddress.ip_network(address)
san = 'IP:{0}/{1}'.format(ip.network_address.compressed, ip.prefixlen)
else:
ip = compat_ipaddress.ip_address(address)
san = 'IP:{0}'.format(ip.compressed)
if san.startswith('Registered ID:'):
san = 'RID:' + san[len('Registered ID:'):]
# Some versions of OpenSSL apparently forgot the colon. Happens in CI with Ubuntu 16.04 and FreeBSD 11.1
if san.startswith('Registered ID'):
san = 'RID:' + san[len('Registered ID'):]
return san
def pyopenssl_get_extensions_from_cert(cert):
# While pyOpenSSL allows us to get an extension's DER value, it won't
# give us the dotted string for an OID. So we have to do some magic to
# get hold of it.
result = dict()
ext_count = cert.get_extension_count()
for i in range(0, ext_count):
ext = cert.get_extension(i)
entry = dict(
critical=bool(ext.get_critical()),
value=base64.b64encode(ext.get_data()),
)
oid = obj2txt(
OpenSSL._util.lib,
OpenSSL._util.ffi,
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
)
# This could also be done a bit simpler:
#
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
#
# Unfortunately this gives the wrong result in case the linked OpenSSL
# doesn't know the OID. That's why we have to get the OID dotted string
# similarly to how cryptography does it.
result[oid] = entry
return result
def pyopenssl_get_extensions_from_csr(csr):
# While pyOpenSSL allows us to get an extension's DER value, it won't
# give us the dotted string for an OID. So we have to do some magic to
# get hold of it.
result = dict()
for ext in csr.get_extensions():
entry = dict(
critical=bool(ext.get_critical()),
value=base64.b64encode(ext.get_data()),
)
oid = obj2txt(
OpenSSL._util.lib,
OpenSSL._util.ffi,
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
)
# This could also be done a bit simpler:
#
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
#
# Unfortunately this gives the wrong result in case the linked OpenSSL
# doesn't know the OID. That's why we have to get the OID dotted string
# similarly to how cryptography does it.
result[oid] = entry
return result
def pyopenssl_parse_name_constraints(name_constraints_extension):
lines = to_text(name_constraints_extension, errors='surrogate_or_strict').splitlines()
exclude = None
excluded = []
permitted = []
for line in lines:
if line.startswith(' ') or line.startswith('\t'):
name = pyopenssl_normalize_name_attribute(line.strip())
if exclude is True:
excluded.append(name)
elif exclude is False:
permitted.append(name)
else:
raise OpenSSLObjectError('Unexpected nameConstraint line: "{0}"'.format(line))
else:
line_lc = line.lower()
if line_lc.startswith('exclud'):
exclude = True
elif line_lc.startswith('includ') or line_lc.startswith('permitt'):
exclude = False
else:
raise OpenSSLObjectError('Cannot parse nameConstraint line: "{0}"'.format(line))
return permitted, excluded

View File

@@ -32,7 +32,7 @@ from ansible.module_utils.common.text.converters import to_native, to_bytes
try:
from OpenSSL import crypto
HAS_PYOPENSSL = True
except ImportError:
except (ImportError, AttributeError):
# Error handled in the calling module.
HAS_PYOPENSSL = False
@@ -98,25 +98,10 @@ def get_fingerprint_of_bytes(source, prefer_one=False):
return fingerprint
def get_fingerprint_of_privatekey(privatekey, backend='pyopenssl', prefer_one=False):
def get_fingerprint_of_privatekey(privatekey, backend='cryptography', prefer_one=False):
"""Generate the fingerprint of the public key. """
if backend == 'pyopenssl':
try:
publickey = crypto.dump_publickey(crypto.FILETYPE_ASN1, privatekey)
except AttributeError:
# If PyOpenSSL < 16.0 crypto.dump_publickey() will fail.
try:
bio = crypto._new_mem_buf()
rc = crypto._lib.i2d_PUBKEY_bio(bio, privatekey._pkey)
if rc != 1:
crypto._raise_current_error()
publickey = crypto._bio_to_string(bio)
except AttributeError:
# By doing this we prevent the code from raising an error
# yet we return no value in the fingerprint hash.
return None
elif backend == 'cryptography':
if backend == 'cryptography':
publickey = privatekey.public_key().public_bytes(
serialization.Encoding.DER,
serialization.PublicFormat.SubjectPublicKeyInfo
@@ -125,7 +110,7 @@ def get_fingerprint_of_privatekey(privatekey, backend='pyopenssl', prefer_one=Fa
return get_fingerprint_of_bytes(publickey, prefer_one=prefer_one)
def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl', prefer_one=False):
def get_fingerprint(path, passphrase=None, content=None, backend='cryptography', prefer_one=False):
"""Generate the fingerprint of the public key. """
privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend)
@@ -133,7 +118,7 @@ def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl', pr
return get_fingerprint_of_privatekey(privatekey, backend=backend, prefer_one=prefer_one)
def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='pyopenssl'):
def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='cryptography'):
"""Load the specified OpenSSL private key.
The content can also be specified via content; in that case,
@@ -177,13 +162,13 @@ def load_privatekey(path, passphrase=None, check_passphrase=True, content=None,
to_bytes('y' if passphrase == 'x' else 'x'))
if passphrase is not None:
# Since we can load the key without an exception, the
# key isn't password-protected
# key is not password-protected
raise OpenSSLBadPassphraseError('Passphrase provided, but private key is not password-protected!')
except crypto.Error as e:
if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0:
if e.args[0][0][2] in ('bad decrypt', 'bad password read'):
# The key is obviously protected by the empty string.
# Don't do this at home (if it's possible at all)...
# Do not do this at home (if it's possible at all)...
raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!')
elif backend == 'cryptography':
try:
@@ -213,14 +198,9 @@ def load_publickey(path=None, content=None, backend=None):
return serialization.load_pem_public_key(content, backend=cryptography_backend())
except Exception as e:
raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e))
else:
try:
return crypto.load_publickey(crypto.FILETYPE_PEM, content)
except crypto.Error as e:
raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e))
def load_certificate(path, content=None, backend='pyopenssl'):
def load_certificate(path, content=None, backend='cryptography'):
"""Load the specified certificate."""
try:
@@ -240,7 +220,7 @@ def load_certificate(path, content=None, backend='pyopenssl'):
raise OpenSSLObjectError(exc)
def load_certificate_request(path, content=None, backend='pyopenssl'):
def load_certificate_request(path, content=None, backend='cryptography'):
"""Load the specified certificate signing request."""
try:
if content is None:
@@ -250,25 +230,50 @@ def load_certificate_request(path, content=None, backend='pyopenssl'):
csr_content = content
except (IOError, OSError) as exc:
raise OpenSSLObjectError(exc)
if backend == 'pyopenssl':
return crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_content)
elif backend == 'cryptography':
if backend == 'cryptography':
try:
return x509.load_pem_x509_csr(csr_content, cryptography_backend())
except ValueError as exc:
raise OpenSSLObjectError(exc)
def parse_name_field(input_dict):
def parse_name_field(input_dict, name_field_name=None):
"""Take a dict with key: value or key: list_of_values mappings and return a list of tuples"""
error_str = '{key}' if name_field_name is None else '{key} in {name}'
result = []
for key, value in input_dict.items():
if isinstance(value, list):
for entry in value:
if not isinstance(entry, six.string_types):
raise TypeError(('Values %s must be strings' % error_str).format(key=key, name=name_field_name))
if not entry:
raise ValueError(('Values for %s must not be empty strings' % error_str).format(key=key))
result.append((key, entry))
elif isinstance(value, six.string_types):
if not value:
raise ValueError(('Value for %s must not be an empty string' % error_str).format(key=key))
result.append((key, value))
else:
raise TypeError(('Value for %s must be either a string or a list of strings' % error_str).format(key=key))
return result
def parse_ordered_name_field(input_list, name_field_name):
"""Take a dict with key: value or key: list_of_values mappings and return a list of tuples"""
result = []
for key in input_dict:
if isinstance(input_dict[key], list):
for entry in input_dict[key]:
result.append((key, entry))
else:
result.append((key, input_dict[key]))
for index, entry in enumerate(input_list):
if len(entry) != 1:
raise ValueError(
'Entry #{index} in {name} must be a dictionary with exactly one key-value pair'.format(
name=name_field_name, index=index + 1))
try:
result.extend(parse_name_field(entry, name_field_name=name_field_name))
except (TypeError, ValueError) as exc:
raise ValueError(
'Error while processing entry #{index} in {name}: {error}'.format(
name=name_field_name, index=index + 1, error=exc))
return result
@@ -322,9 +327,7 @@ def get_relative_time_option(input_string, input_name, backend='cryptography'):
elif backend == 'cryptography':
return result_datetime
# Absolute time
if backend == 'pyopenssl':
return input_string
elif backend == 'cryptography':
if backend == 'cryptography':
for date_fmt in ['%Y%m%d%H%M%SZ', '%Y%m%d%H%MZ', '%Y%m%d%H%M%S%z', '%Y%m%d%H%M%z']:
try:
return datetime.datetime.strptime(result, date_fmt)

View File

@@ -7,7 +7,7 @@
# their own license to the complete work.
#
# Copyright (c), Entrust Datacard Corporation, 2019
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
@@ -34,7 +34,6 @@ __metaclass__ = type
import json
import os
import re
import time
import traceback
from ansible.module_utils.common.text.converters import to_text, to_native
@@ -105,7 +104,7 @@ def bind(instance, method, operation_spec):
def binding_scope_fn(*args, **kwargs):
return method(instance, *args, **kwargs)
# Make sure we don't confuse users; add the proper name and documentation to the function.
# Make sure we do not confuse users; add the proper name and documentation to the function.
# Users can use !help(<function>) to get help on the function from interactive python or pdb
operation_name = operation_spec.get("operationId").split("Using")[0]
binding_scope_fn.__name__ = str(operation_name)

View File

@@ -24,6 +24,19 @@ import os
import tempfile
def load_file(path, module=None):
'''
Load the file as a bytes string.
'''
try:
with open(path, 'rb') as f:
return f.read()
except Exception as exc:
if module is None:
raise
module.fail_json('Error while loading {0} - {1}'.format(path, str(exc)))
def load_file_if_exists(path, module=None, ignore_errors=False):
'''
Load the file as a bytes string. If the file does not exist, ``None`` is returned.

View File

@@ -21,9 +21,11 @@ __metaclass__ = type
import abc
import os
import stat
import traceback
from ansible.module_utils import six
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
parse_openssh_version,
)
@@ -75,7 +77,14 @@ class OpensshModule(object):
self.check_mode = self.module.check_mode
def execute(self):
self._execute()
try:
self._execute()
except Exception as e:
self.module.fail_json(
msg="unexpected error occurred: %s" % to_native(e),
exception=traceback.format_exc(),
)
self.module.exit_json(**self.result)
@abc.abstractmethod

View File

@@ -21,12 +21,13 @@ __metaclass__ = type
import abc
import os
from distutils.version import LooseVersion
from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.openssh.cryptography import (
HAS_OPENSSH_SUPPORT,
HAS_OPENSSH_PRIVATE_FORMAT,

View File

@@ -20,10 +20,11 @@ __metaclass__ = type
import os
from base64 import b64encode, b64decode
from distutils.version import LooseVersion
from getpass import getuser
from socket import gethostname
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
try:
from cryptography import __version__ as CRYPTOGRAPHY_VERSION
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm

View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""Provide version object to compare version numbers."""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
# Once we drop support for Ansible 2.9, ansible-base 2.10, and ansible-core 2.11, we can
# remove the _version.py file, and replace the following import by
#
# from ansible.module_utils.compat.version import LooseVersion
from ._version import LooseVersion

View File

@@ -133,7 +133,7 @@ EXAMPLES = '''
- mailto:me@example.com
- mailto:myself@example.org
- name: Make sure account has given email address. Don't create account if it doesn't exist
- name: Make sure account has given email address. Do not create account if it does not exist
community.crypto.acme_account:
account_key_src: /etc/pki/cert/private/account.key
state: present

View File

@@ -1 +0,0 @@
acme_account_info.py

View File

@@ -31,9 +31,8 @@ options:
by the ACME server."
- "A value of C(ignore) will not fetch the list of orders."
- "If the value is not C(ignore) and the ACME server supports orders, the C(order_uris)
return value is always populated. The C(orders) return value currently depends on
whether this option is set to C(url_list) or C(object_list). In community.crypto 2.0.0,
it will only be returned if this option is set to C(object_list)."
return value is always populated. The C(orders) return value is only returned
if this option is set to C(object_list)."
- "Currently, Let's Encrypt does not return orders, so the C(orders) result
will always be empty."
type: str
@@ -54,7 +53,7 @@ EXAMPLES = '''
- name: Check whether an account with the given account key exists
community.crypto.acme_account_info:
account_key_src: /etc/pki/cert/private/account.key
register: account_data
register: account_data
- name: Verify that account exists
assert:
that:
@@ -70,7 +69,7 @@ EXAMPLES = '''
acme_account_info:
account_key_content: "{{ acme_account_key }}"
account_uri: "{{ acme_account_uri }}"
register: account_data
register: account_data
- name: Verify that account exists
assert:
that:
@@ -125,12 +124,9 @@ account:
orders:
description:
- "The list of orders."
- "If I(retrieve_orders) is C(url_list), this will be a list of URLs. In community.crypto 2.0.0,
this return value will no longer be returned for C(url_list)."
- "If I(retrieve_orders) is C(object_list), this will be a list of objects."
type: list
#elements: ... depends on retrieve_orders
returned: if account exists, I(retrieve_orders) is not C(ignore), and server supports order listing
elements: dict
returned: if account exists, I(retrieve_orders) is C(object_list), and server supports order listing
contains:
status:
description: The order's status.
@@ -282,9 +278,6 @@ def main():
),
supports_check_mode=True,
)
if module._name in ('acme_account_facts', 'community.crypto.acme_account_facts'):
module.deprecate("The 'acme_account_facts' module has been renamed to 'acme_account_info'",
version='2.0.0', collection_name='community.crypto')
backend = create_backend(module, True)
try:
@@ -313,13 +306,6 @@ def main():
if account_data.get('orders') and module.params['retrieve_orders'] != 'ignore':
orders = get_orders_list(module, client, account_data['orders'])
result['order_uris'] = orders
if module.params['retrieve_orders'] == 'url_list':
module.deprecate(
'retrieve_orders=url_list now returns the order URI list as `order_uris`.'
' Right now it also returns this list as `orders` for backwards compatibility,'
' but this will stop in community.crypto 2.0.0',
version='2.0.0', collection_name='community.crypto')
result['orders'] = orders
if module.params['retrieve_orders'] == 'object_list':
result['orders'] = [get_order(client, order) for order in orders]
module.exit_json(**result)

View File

@@ -170,7 +170,7 @@ options:
aliases: ['cert']
fullchain_dest:
description:
- "The destination file for the full chain (i.e. certificate followed
- "The destination file for the full chain (that is, a certificate followed
by chain of intermediate certificates)."
- "Required if C(dest) is not specified."
type: path
@@ -204,7 +204,7 @@ options:
description:
- Enforces the execution of the challenge and validation, even if an
existing certificate is still valid for more than C(remaining_days).
- This is especially helpful when having an updated CSR e.g. with
- This is especially helpful when having an updated CSR, for example with
additional domains for which a new certificate is desired.
type: bool
default: no
@@ -238,9 +238,9 @@ options:
- "Determines which certificates in the chain will be tested."
- "I(all) tests all certificates in the chain (excluding the leaf, which is
identical in all chains)."
- "I(first) only tests the first certificate in the chain, i.e. the one which
- "I(first) only tests the first certificate in the chain, that is the one which
signed the leaf."
- "I(last) only tests the last certificate in the chain, i.e. the one furthest
- "I(last) only tests the last certificate in the chain, that is the one furthest
away from the leaf. Its issuer is the root certificate of this chain."
type: str
default: all
@@ -308,7 +308,7 @@ EXAMPLES = r'''
# - copy:
# dest: /var/www/html/{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource'] }}
# content: "{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}"
# when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge['challenge_data']
# when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge['challenge_data']
#
# Alternative way:
#
@@ -580,9 +580,12 @@ class ACMECertificateClient(object):
if self.module.params['select_chain']:
for criterium_idx, criterium in enumerate(self.module.params['select_chain']):
self.select_chain_matcher.append(
self.client.backend.create_chain_matcher(
Criterium(criterium, index=criterium_idx)))
try:
self.select_chain_matcher.append(
self.client.backend.create_chain_matcher(
Criterium(criterium, index=criterium_idx)))
except ValueError as exc:
self.module.warn('Error while parsing criterium: {error}. Ignoring criterium.'.format(error=exc))
# Make sure account exists
modify_account = module.params['modify_account']

View File

@@ -225,7 +225,7 @@ def main():
if result.get('type') == error_type and result.get('detail') == 'Certificate already revoked':
# Fallback: boulder returns this in case the certificate was already revoked.
already_revoked = True
# If we know the certificate was already revoked, we don't fail,
# If we know the certificate was already revoked, we do not fail,
# but successfully terminate while indicating no change
if already_revoked:
module.exit_json(changed=False)

View File

@@ -145,6 +145,8 @@ import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
from ansible_collections.community.crypto.plugins.module_utils.acme.io import (
@@ -164,7 +166,6 @@ try:
import cryptography.x509
import cryptography.x509.oid
import ipaddress
from distutils.version import LooseVersion
HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.3'))
_cryptography_backend = cryptography.hazmat.backends.default_backend()
except ImportError as dummy:

View File

@@ -124,6 +124,8 @@ import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
split_pem_list,
)
@@ -131,6 +133,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
import cryptography.exceptions
import cryptography.hazmat.backends
import cryptography.hazmat.primitives.serialization
import cryptography.hazmat.primitives.asymmetric.rsa
@@ -140,7 +143,6 @@ try:
import cryptography.hazmat.primitives.asymmetric.utils
import cryptography.x509
import cryptography.x509.oid
from distutils.version import LooseVersion
HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.5'))
_cryptography_backend = cryptography.hazmat.backends.default_backend()
except ImportError as dummy:
@@ -189,6 +191,9 @@ def is_parent(module, cert, potential_parent):
return True
except cryptography.exceptions.InvalidSignature as dummy:
return False
except cryptography.exceptions.UnsupportedAlgorithm as dummy:
module.warn('Unsupported algorithm "{0}"'.format(cert.cert.signature_hash_algorithm))
return False
except Exception as e:
module.fail_json(msg='Unknown error on signature validation: {0}'.format(e))
@@ -236,13 +241,17 @@ class CertificateSet(object):
def __init__(self, module):
self.module = module
self.certificates = set()
self.certificate_by_issuer = dict()
self.certificates_by_issuer = dict()
self.certificate_by_cert = dict()
def _load_file(self, path):
certs = load_PEM_list(self.module, path, fail_on_error=False)
for cert in certs:
self.certificates.add(cert)
self.certificate_by_issuer[cert.cert.subject] = cert
if cert.cert.subject not in self.certificates_by_issuer:
self.certificates_by_issuer[cert.cert.subject] = []
self.certificates_by_issuer[cert.cert.subject].append(cert)
self.certificate_by_cert[cert.cert] = cert
def load(self, path):
'''
@@ -260,8 +269,8 @@ class CertificateSet(object):
'''
Search for the parent (issuer) of a certificate. Return ``None`` if none was found.
'''
potential_parent = self.certificate_by_issuer.get(cert.cert.issuer)
if potential_parent is not None:
potential_parents = self.certificates_by_issuer.get(cert.cert.issuer, [])
for potential_parent in potential_parents:
if is_parent(self.module, cert, potential_parent):
return potential_parent
return None
@@ -274,6 +283,16 @@ def format_cert(cert):
return str(cert.cert)
def check_cycle(module, occured_certificates, next):
'''
Make sure that next is not in occured_certificates so far, and add it.
'''
next_cert = next.cert
if next_cert in occured_certificates:
module.fail_json(msg='Found cycle while building certificate chain')
occured_certificates.add(next_cert)
def main():
module = AnsibleModule(
argument_spec=dict(
@@ -312,13 +331,19 @@ def main():
# Try to complete chain
current = chain[-1]
completed = []
occured_certificates = set([cert.cert for cert in chain])
if current.cert in roots.certificate_by_cert:
# Do not try to complete the chain when it's already ending with a root certificate
current = None
while current:
root = roots.find_parent(current)
if root:
check_cycle(module, occured_certificates, root)
completed.append(root)
break
intermediate = intermediates.find_parent(current)
if intermediate:
check_cycle(module, occured_certificates, intermediate)
completed.append(intermediate)
current = intermediate
else:

View File

@@ -0,0 +1,335 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2021 Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: crypto_info
author: "Felix Fontein (@felixfontein)"
short_description: Retrieve cryptographic capabilities
version_added: 2.1.0
description:
- Retrieve information on cryptographic capabilities.
- The current version retrieves information on the L(Python cryptography library, https://cryptography.io/) available to
Ansible modules, and on the OpenSSL binary C(openssl) found in the path.
notes:
- Supports C(check_mode).
options: {}
'''
EXAMPLES = '''
- name: Retrieve information
community.crypto.crypto_info:
account_key_src: /etc/pki/cert/private/account.key
register: crypto_information
- name: Show retrieved information
ansible.builtin.debug:
var: crypto_information
'''
RETURN = '''
python_cryptography_installed:
description: Whether the L(Python cryptography library, https://cryptography.io/) is installed.
returned: always
type: bool
sample: true
python_cryptography_import_error:
description: Import error when trying to import the L(Python cryptography library, https://cryptography.io/).
returned: when I(python_cryptography_installed=false)
type: str
python_cryptography_capabilities:
description: Information on the installed L(Python cryptography library, https://cryptography.io/).
returned: when I(python_cryptography_installed=true)
type: dict
contains:
version:
description: The library version.
type: str
curves:
description:
- List of all supported elliptic curves.
- Theoretically this should be non-empty for version 0.5 and higher, depending on the libssl version used.
type: list
elements: str
has_ec:
description:
- Whether elliptic curves are supported.
- Theoretically this should be the case for version 0.5 and higher, depending on the libssl version used.
type: bool
has_ec_sign:
description:
- Whether signing with elliptic curves is supported.
- Theoretically this should be the case for version 1.5 and higher, depending on the libssl version used.
type: bool
has_ed25519:
description:
- Whether Ed25519 keys are supported.
- Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used.
type: bool
has_ed25519_sign:
description:
- Whether signing with Ed25519 keys is supported.
- Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used.
type: bool
has_ed448:
description:
- Whether Ed448 keys are supported.
- Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used.
type: bool
has_ed448_sign:
description:
- Whether signing with Ed448 keys is supported.
- Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used.
type: bool
has_dsa:
description:
- Whether DSA keys are supported.
- Theoretically this should be the case for version 0.5 and higher.
type: bool
has_dsa_sign:
description:
- Whether signing with DSA keys is supported.
- Theoretically this should be the case for version 1.5 and higher.
type: bool
has_rsa:
description:
- Whether RSA keys are supported.
- Theoretically this should be the case for version 0.5 and higher.
type: bool
has_rsa_sign:
description:
- Whether signing with RSA keys is supported.
- Theoretically this should be the case for version 1.4 and higher.
type: bool
has_x25519:
description:
- Whether X25519 keys are supported.
- Theoretically this should be the case for version 2.0 and higher, depending on the libssl version used.
type: bool
has_x25519_serialization:
description:
- Whether serialization of X25519 keys is supported.
- Theoretically this should be the case for version 2.5 and higher, depending on the libssl version used.
type: bool
has_x448:
description:
- Whether X448 keys are supported.
- Theoretically this should be the case for version 2.5 and higher, depending on the libssl version used.
type: bool
openssl_present:
description: Whether the OpenSSL binary C(openssl) is installed and can be found in the PATH.
returned: always
type: bool
sample: true
openssl:
description: Information on the installed OpenSSL binary.
returned: when I(openssl_present=true)
type: dict
contains:
path:
description: Path of the OpenSSL binary.
type: str
sample: /usr/bin/openssl
version:
description: The OpenSSL version.
type: str
sample: 1.1.1m
version_output:
description: The complete output of C(openssl version).
type: str
sample: 'OpenSSL 1.1.1m 14 Dec 2021\n'
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
CRYPTOGRAPHY_HAS_EC,
CRYPTOGRAPHY_HAS_EC_SIGN,
CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_ED25519_SIGN,
CRYPTOGRAPHY_HAS_ED448,
CRYPTOGRAPHY_HAS_ED448_SIGN,
CRYPTOGRAPHY_HAS_DSA,
CRYPTOGRAPHY_HAS_DSA_SIGN,
CRYPTOGRAPHY_HAS_RSA,
CRYPTOGRAPHY_HAS_RSA_SIGN,
CRYPTOGRAPHY_HAS_X25519,
CRYPTOGRAPHY_HAS_X25519_FULL,
CRYPTOGRAPHY_HAS_X448,
HAS_CRYPTOGRAPHY,
)
try:
import cryptography
from cryptography.exceptions import UnsupportedAlgorithm
except ImportError:
UnsupportedAlgorithm = Exception
CRYPTOGRAPHY_VERSION = None
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
else:
CRYPTOGRAPHY_VERSION = cryptography.__version__
CRYPTOGRAPHY_IMP_ERR = None
CURVES = (
('secp224r1', 'SECP224R1'),
('secp256k1', 'SECP256K1'),
('secp256r1', 'SECP256R1'),
('secp384r1', 'SECP384R1'),
('secp521r1', 'SECP521R1'),
('secp192r1', 'SECP192R1'),
('sect163k1', 'SECT163K1'),
('sect163r2', 'SECT163R2'),
('sect233k1', 'SECT233K1'),
('sect233r1', 'SECT233R1'),
('sect283k1', 'SECT283K1'),
('sect283r1', 'SECT283R1'),
('sect409k1', 'SECT409K1'),
('sect409r1', 'SECT409R1'),
('sect571k1', 'SECT571K1'),
('sect571r1', 'SECT571R1'),
('brainpoolP256r1', 'BrainpoolP256R1'),
('brainpoolP384r1', 'BrainpoolP384R1'),
('brainpoolP512r1', 'BrainpoolP512R1'),
)
def add_crypto_information(module):
result = {}
result['python_cryptography_installed'] = HAS_CRYPTOGRAPHY
if not HAS_CRYPTOGRAPHY:
result['python_cryptography_import_error'] = CRYPTOGRAPHY_IMP_ERR
return result
has_ed25519 = CRYPTOGRAPHY_HAS_ED25519
if has_ed25519:
try:
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
Ed25519PrivateKey.from_private_bytes(b'')
except ValueError:
pass
except UnsupportedAlgorithm:
has_ed25519 = False
has_ed448 = CRYPTOGRAPHY_HAS_ED448
if has_ed448:
try:
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey
Ed448PrivateKey.from_private_bytes(b'')
except ValueError:
pass
except UnsupportedAlgorithm:
has_ed448 = False
has_x25519 = CRYPTOGRAPHY_HAS_X25519
if has_x25519:
try:
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
if CRYPTOGRAPHY_HAS_X25519_FULL:
X25519PrivateKey.from_private_bytes(b'')
else:
# Some versions do not support serialization and deserialization - use generate() instead
X25519PrivateKey.generate()
except ValueError:
pass
except UnsupportedAlgorithm:
has_x25519 = False
has_x448 = CRYPTOGRAPHY_HAS_X448
if has_x448:
try:
from cryptography.hazmat.primitives.asymmetric.x448 import X448PrivateKey
X448PrivateKey.from_private_bytes(b'')
except ValueError:
pass
except UnsupportedAlgorithm:
has_x448 = False
curves = []
if CRYPTOGRAPHY_HAS_EC:
import cryptography.hazmat.backends
import cryptography.hazmat.primitives.asymmetric.ec
backend = cryptography.hazmat.backends.default_backend()
for curve_name, constructor_name in CURVES:
ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(constructor_name)
if ecclass:
try:
cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(curve=ecclass(), backend=backend)
curves.append(curve_name)
except UnsupportedAlgorithm:
pass
info = {
'version': CRYPTOGRAPHY_VERSION,
'curves': curves,
'has_ec': CRYPTOGRAPHY_HAS_EC,
'has_ec_sign': CRYPTOGRAPHY_HAS_EC_SIGN,
'has_ed25519': has_ed25519,
'has_ed25519_sign': has_ed25519 and CRYPTOGRAPHY_HAS_ED25519_SIGN,
'has_ed448': has_ed448,
'has_ed448_sign': has_ed448 and CRYPTOGRAPHY_HAS_ED448_SIGN,
'has_dsa': CRYPTOGRAPHY_HAS_DSA,
'has_dsa_sign': CRYPTOGRAPHY_HAS_DSA_SIGN,
'has_rsa': CRYPTOGRAPHY_HAS_RSA,
'has_rsa_sign': CRYPTOGRAPHY_HAS_RSA_SIGN,
'has_x25519': has_x25519,
'has_x25519_serialization': has_x25519 and CRYPTOGRAPHY_HAS_X25519_FULL,
'has_x448': has_x448,
}
result['python_cryptography_capabilities'] = info
return result
def add_openssl_information(module):
openssl_binary = module.get_bin_path('openssl')
result = {
'openssl_present': openssl_binary is not None,
}
if openssl_binary is None:
return result
openssl_result = {
'path': openssl_binary,
}
result['openssl'] = openssl_result
rc, out, err = module.run_command([openssl_binary, 'version'])
if rc == 0:
openssl_result['version_output'] = out
parts = out.split(None, 2)
if len(parts) > 1:
openssl_result['version'] = parts[1]
return result
INFO_FUNCTIONS = (
add_crypto_information,
add_openssl_information,
)
def main():
module = AnsibleModule(argument_spec={}, supports_check_mode=True)
result = {}
for fn in INFO_FUNCTIONS:
result.update(fn(module))
module.exit_json(**result)
if __name__ == '__main__':
main()

View File

@@ -81,8 +81,8 @@ options:
- The number of days the certificate must have left being valid. If C(cert_days < remaining_days) then a new certificate will be
obtained using I(request_type).
- If C(request_type=renew), a renewal will fail if the certificate being renewed has been issued within the past 30 days, so do not set a
I(remaining_days) value that is within 30 days of the full lifetime of the certificate being acted upon. (e.g. if you are requesting Certificates
with a 90 day lifetime, do not set remaining_days to a value C(60) or higher).
I(remaining_days) value that is within 30 days of the full lifetime of the certificate being acted upon.
- For exmaple, if you are requesting Certificates with a 90 day lifetime, do not set I(remaining_days) to a value C(60) or higher).
- The I(force) option may be used to ensure that a new certificate is always obtained.
type: int
default: 30
@@ -97,8 +97,8 @@ options:
- Specifying C(request_type=reissue) means that an existing certificate (specified by I(tracking_id) if present, otherwise I(path)) will be
reissued.
If there is no certificate to reissue, a new certificate is requested.
- If a certificate was issued within the past 30 days, the 'renew' operation is not a valid operation and will fail.
- Note that C(reissue) is an operation that will result in the revocation of the certificate that is reissued, be cautious with it's use.
- If a certificate was issued within the past 30 days, the C(renew) operation is not a valid operation and will fail.
- Note that C(reissue) is an operation that will result in the revocation of the certificate that is reissued, be cautious with its use.
- I(check_mode) is only supported if C(request_type=new)
- For example, setting C(request_type=renew) and C(remaining_days=30) and pointing to the same certificate on multiple playbook runs means that on
the first run new certificate will be requested. It will then be left along on future runs until it is within 30 days of expiry, then the
@@ -519,11 +519,11 @@ import re
import time
import traceback
from distutils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_native, to_bytes
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.io import (
write_file,
)
@@ -690,7 +690,7 @@ class EcsCertificate(object):
if len(cert_results) == 1:
self.tracking_id = cert_results[0].get('trackingId')
except RestOperationException as dummy:
# If we fail to find a cert by serial number, that's fine, we just don't set self.tracking_id
# If we fail to find a cert by serial number, that's fine, we just do not set self.tracking_id
return
def set_cert_details(self, module):

View File

@@ -63,11 +63,11 @@ options:
description:
- Email address to be used to verify domain ownership.
- 'Email address must be either an email address present in the WHOIS data for I(domain_name), or one of the following constructed emails:
admin@I(domain_name), administrator@I(domain_name), webmaster@I(domain_name), hostmaster@I(domain_name), postmaster@I(domain_name)'
admin@I(domain_name), administrator@I(domain_name), webmaster@I(domain_name), hostmaster@I(domain_name), postmaster@I(domain_name).'
- 'Note that if I(domain_name) includes subdomains, the top level domain should be used. For example, if requesting validation of
example1.ansible.com, or test.example2.ansible.com, and you want to use the "admin" preconstructed name, the email address should be
admin@ansible.com.'
- If using the email values from the WHOIS data for the domain or it's top level namespace, they must be exact matches.
- If using the email values from the WHOIS data for the domain or its top level namespace, they must be exact matches.
- If C(verification_method=email) but I(verification_email) is not provided, the first email address found in WHOIS data for the domain will be
used.
- To verify domain ownership, domain owner must follow the instructions in the email they receive.

View File

@@ -14,10 +14,7 @@ author: "John Westcott IV (@john-westcott-iv)"
short_description: Get a certificate from a host:port
description:
- Makes a secure connection and returns information about the presented certificate
- "The module can use the cryptography Python library, or the pyOpenSSL Python
library. By default, it tries to detect which one is available. This can be
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL
backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
- The module uses the cryptography Python library.
- Support SNI (L(Server Name Indication,https://en.wikipedia.org/wiki/Server_Name_Indication)) only with python >= 2.7.
options:
host:
@@ -66,19 +63,18 @@ options:
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [ auto, cryptography, pyopenssl ]
choices: [ auto, cryptography ]
notes:
- When using ca_cert on OS X it has been reported that in some conditions the validate will always succeed.
requirements:
- "python >= 2.7 when using C(proxy_host)"
- "cryptography >= 1.6 or pyOpenSSL >= 0.15"
- "cryptography >= 1.6"
'''
RETURN = '''
@@ -103,7 +99,13 @@ extensions:
asn1_data:
returned: success
type: str
description: The Base64 encoded ASN.1 content of the extnesion.
description:
- The Base64 encoded ASN.1 content of the extension.
- B(Note) that depending on the C(cryptography) version used, it is
not possible to extract the ASN.1 content of the extension, but only
to provide the re-encoded content of the extension in case it was
parsed by C(cryptography). This should usually result in exactly the
same value, except if the original extension value was malformed.
name:
returned: success
type: str
@@ -167,7 +169,6 @@ import base64
import datetime
import traceback
from distutils.version import LooseVersion
from os.path import isfile
from socket import create_connection, setdefaulttimeout, socket
from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_REQUIRED
@@ -175,12 +176,13 @@ from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_RE
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
cryptography_oid_to_name,
cryptography_get_extensions_from_cert,
)
MINIMAL_PYOPENSSL_VERSION = '0.15'
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
CREATE_DEFAULT_CONTEXT_IMP_ERR = None
@@ -192,17 +194,6 @@ except ImportError:
else:
HAS_CREATE_DEFAULT_CONTEXT = True
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
except ImportError:
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
PYOPENSSL_FOUND = True
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
@@ -241,7 +232,7 @@ def main():
proxy_port=dict(type='int', default=8080),
server_name=dict(type='str'),
timeout=dict(type='int', default=10),
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
starttls=dict(type='str', choices=['mysql']),
),
)
@@ -259,28 +250,17 @@ def main():
if backend == 'auto':
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
# First try cryptography, then pyOpenSSL
# Try cryptography
if can_use_cryptography:
backend = 'cryptography'
elif can_use_pyopenssl:
backend = 'pyopenssl'
# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION))
module.fail_json(msg=("Cannot detect the required Python library "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == 'pyopenssl':
if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR)
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.0.0', collection_name='community.crypto')
elif backend == 'cryptography':
if backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
@@ -343,37 +323,7 @@ def main():
result['cert'] = cert
if backend == 'pyopenssl':
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
result['subject'] = {}
for component in x509.get_subject().get_components():
result['subject'][component[0]] = component[1]
result['expired'] = x509.has_expired()
result['extensions'] = []
extension_count = x509.get_extension_count()
for index in range(0, extension_count):
extension = x509.get_extension(index)
result['extensions'].append({
'critical': extension.get_critical(),
'asn1_data': extension.get_data(),
'name': extension.get_short_name(),
})
result['issuer'] = {}
for component in x509.get_issuer().get_components():
result['issuer'][component[0]] = component[1]
result['not_after'] = x509.get_notAfter()
result['not_before'] = x509.get_notBefore()
result['serial_number'] = x509.get_serial_number()
result['signature_algorithm'] = x509.get_signature_algorithm()
result['version'] = x509.get_version()
elif backend == 'cryptography':
if backend == 'cryptography':
x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography_backend())
result['subject'] = {}
for attribute in x509.subject:

View File

@@ -21,7 +21,7 @@ description:
options:
device:
description:
- "Device to work with (e.g. C(/dev/sda1)). Needed in most cases.
- "Device to work with (for example C(/dev/sda1)). Needed in most cases.
Can be omitted only when I(state=closed) together with I(name)
is provided."
type: str
@@ -53,7 +53,7 @@ options:
description:
- "Sets container name when I(state=opened). Can be used
instead of I(device) when closing the existing container
(i.e. when I(state=closed))."
(that is, when I(state=closed))."
type: str
keyfile:
description:
@@ -159,7 +159,7 @@ options:
- "This option allows the user to define the cipher specification
string for the LUKS container."
- "Will only be used on container creation."
- "For pre-2.6.10 kernels, use C(aes-plain) as they don't understand
- "For pre-2.6.10 kernels, use C(aes-plain) as they do not understand
the new cipher spec strings. To use ESSIV, use C(aes-cbc-essiv:sha256)."
type: str
version_added: '1.1.0'
@@ -217,6 +217,48 @@ options:
- "Will only be used on container creation."
type: int
version_added: '1.5.0'
perf_same_cpu_crypt:
description:
- "Allows the user to perform encryption using the same CPU that IO was submitted on."
- "The default is to use an unbound workqueue so that encryption work is automatically balanced between available CPUs."
- "Will only be used when opening containers."
type: bool
default: false
version_added: '2.3.0'
perf_submit_from_crypt_cpus:
description:
- "Allows the user to disable offloading writes to a separate thread after encryption."
- "There are some situations where offloading block write IO operations from the encryption threads
to a single thread degrades performance significantly."
- "The default is to offload block write IO operations to the same thread."
- "Will only be used when opening containers."
type: bool
default: false
version_added: '2.3.0'
perf_no_read_workqueue:
description:
- "Allows the user to bypass dm-crypt internal workqueue and process read requests synchronously."
- "Will only be used when opening containers."
type: bool
default: false
version_added: '2.3.0'
perf_no_write_workqueue:
description:
- "Allows the user to bypass dm-crypt internal workqueue and process write requests synchronously."
- "Will only be used when opening containers."
type: bool
default: false
version_added: '2.3.0'
persistent:
description:
- "Allows the user to store options into container's metadata persistently and automatically use them next time.
Only I(perf_same_cpu_crypt), I(perf_submit_from_crypt_cpus), I(perf_no_read_workqueue), and I(perf_no_write_workqueue)
can be stored persistently."
- "Will only work with LUKS2 containers."
- "Will only be used when opening containers."
type: bool
default: false
version_added: '2.3.0'
requirements:
- "cryptsetup"
@@ -350,12 +392,40 @@ STDERR = 2
# used to get <luks-name> out of lsblk output in format 'crypt <luks-name>'
# regex takes care of any possible blank characters
LUKS_NAME_REGEX = re.compile(r'\s*crypt\s+([^\s]*)\s*')
LUKS_NAME_REGEX = re.compile(r'^crypt\s+([^\s]*)\s*$')
# used to get </luks/device> out of lsblk output
# in format 'device: </luks/device>'
LUKS_DEVICE_REGEX = re.compile(r'\s*device:\s+([^\s]*)\s*')
# See https://gitlab.com/cryptsetup/cryptsetup/-/wikis/LUKS-standard/on-disk-format.pdf
LUKS_HEADER = b'LUKS\xba\xbe'
LUKS_HEADER_L = 6
# See https://gitlab.com/cryptsetup/LUKS2-docs/-/blob/master/luks2_doc_wip.pdf
LUKS2_HEADER_OFFSETS = [0x4000, 0x8000, 0x10000, 0x20000, 0x40000, 0x80000, 0x100000, 0x200000, 0x400000]
LUKS2_HEADER2 = b'SKUL\xba\xbe'
def wipe_luks_headers(device):
wipe_offsets = []
with open(device, 'rb') as f:
# f.seek(0)
data = f.read(LUKS_HEADER_L)
if data == LUKS_HEADER:
wipe_offsets.append(0)
for offset in LUKS2_HEADER_OFFSETS:
f.seek(offset)
data = f.read(LUKS_HEADER_L)
if data == LUKS2_HEADER2:
wipe_offsets.append(offset)
if wipe_offsets:
with open(device, 'wb') as f:
for offset in wipe_offsets:
f.seek(offset)
f.write(b'\x00\x00\x00\x00\x00\x00')
class Handler(object):
def __init__(self, module):
@@ -418,13 +488,11 @@ class CryptHandler(Handler):
raise ValueError('Error while obtaining LUKS name for %s: %s'
% (device, result[STDERR]))
m = LUKS_NAME_REGEX.search(result[STDOUT])
try:
name = m.group(1)
except AttributeError:
name = None
return name
for line in result[STDOUT].splitlines(False):
m = LUKS_NAME_REGEX.match(line)
if m:
return m.group(1)
return None
def get_container_device_by_name(self, name):
''' obtain device name based on the LUKS container name
@@ -491,10 +559,21 @@ class CryptHandler(Handler):
raise ValueError('Error while creating LUKS on %s: %s'
% (device, result[STDERR]))
def run_luks_open(self, device, keyfile, passphrase, name):
def run_luks_open(self, device, keyfile, passphrase, perf_same_cpu_crypt, perf_submit_from_crypt_cpus,
perf_no_read_workqueue, perf_no_write_workqueue, persistent, name):
args = [self._cryptsetup_bin]
if keyfile:
args.extend(['--key-file', keyfile])
if perf_same_cpu_crypt:
args.extend(['--perf-same_cpu_crypt'])
if perf_submit_from_crypt_cpus:
args.extend(['--perf-submit_from_crypt_cpus'])
if perf_no_read_workqueue:
args.extend(['--perf-no_read_workqueue'])
if perf_no_write_workqueue:
args.extend(['--perf-no_write_workqueue'])
if persistent:
args.extend(['--persistent'])
args.extend(['open', '--type', 'luks', device, name])
result = self._run_command(args, data=passphrase)
@@ -515,9 +594,17 @@ class CryptHandler(Handler):
self.run_luks_close(name)
result = self._run_command([wipefs_bin, '--all', device])
if result[RETURN_CODE] != 0:
raise ValueError('Error while wiping luks container %s: %s'
raise ValueError('Error while wiping LUKS container signatures for %s: %s'
% (device, result[STDERR]))
# For LUKS2, sometimes both `cryptsetup erase` and `wipefs` do **not**
# erase all LUKS signatures (they seem to miss the second header). That's
# why we do it ourselves here.
try:
wipe_luks_headers(device)
except Exception as exc:
raise ValueError('Error while wiping LUKS container signatures for %s: %s' % (device, exc))
def run_luks_add_key(self, device, keyfile, passphrase, new_keyfile,
new_passphrase, pbkdf):
''' Add new key from a keyfile or passphrase to given 'device';
@@ -768,6 +855,11 @@ def run_module():
mutually_exclusive=[('iteration_time', 'iteration_count')],
),
sector_size=dict(type='int'),
perf_same_cpu_crypt=dict(type='bool', default=False),
perf_submit_from_crypt_cpus=dict(type='bool', default=False),
perf_no_read_workqueue=dict(type='bool', default=False),
perf_no_write_workqueue=dict(type='bool', default=False),
persistent=dict(type='bool', default=False),
)
mutually_exclusive = [
@@ -785,6 +877,7 @@ def run_module():
module = AnsibleModule(argument_spec=module_args,
supports_check_mode=True,
mutually_exclusive=mutually_exclusive)
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
if module.params['device'] is not None:
try:
@@ -842,6 +935,11 @@ def run_module():
crypt.run_luks_open(conditions.device,
module.params['keyfile'],
module.params['passphrase'],
module.params['perf_same_cpu_crypt'],
module.params['perf_submit_from_crypt_cpus'],
module.params['perf_no_read_workqueue'],
module.params['perf_no_write_workqueue'],
module.params['persistent'],
name)
except ValueError as e:
module.fail_json(msg="luks_device error: %s" % e)

View File

@@ -50,6 +50,7 @@ options:
match the module's options.
- When C(partial_idempotence) an existing certificate will be regenerated based on
I(serial), I(signature_algorithm), I(type), I(valid_from), I(valid_to), I(valid_at), and I(principals).
I(valid_from) and I(valid_to) can be excluded by I(ignore_timestamps=true).
- When C(full_idempotence) I(identifier), I(options), I(public_key), and I(signing_key)
are also considered when compared against an existing certificate.
- C(always) is equivalent to I(force=true).
@@ -110,16 +111,20 @@ options:
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 formats are: C([+-]timespec | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS | YYYY-MM-DD HH:MM:SS | always)
where timespec can be an integer + C([w | d | h | m | s]) (e.g. C(+32w1d2h).
where timespec can be an integer + C([w | d | h | m | s]) (for example C(+32w1d2h)).
Note that if using relative time this module is NOT idempotent."
- "The value C(always) is only supported for OpenSSH 7.7 and greater, however, the value C(1970-01-01T00:00:01)
can be used with earlier versions as an equivalent expression."
- "To ignore this value during comparison with an existing certificate set I(ignore_timestamps=true)."
- Required if I(state) is C(present).
type: str
valid_to:
description:
- "The point in time the certificate is valid to. Time can be specified either as relative time or as absolute timestamp.
Time will always be interpreted as UTC. Valid formats are: C([+-]timespec | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS | YYYY-MM-DD HH:MM:SS | forever)
where timespec can be an integer + C([w | d | h | m | s]) (e.g. C(+32w1d2h).
where timespec can be an integer + C([w | d | h | m | s]) (for example C(+32w1d2h)).
Note that if using relative time this module is NOT idempotent."
- "To ignore this value during comparison with an existing certificate set I(ignore_timestamps=true)."
- Required if I(state) is C(present).
type: str
valid_at:
@@ -128,6 +133,13 @@ options:
Time will always be interpreted as UTC. Mainly to be used with relative timespec for I(valid_from) and / or I(valid_to).
Note that if using relative time this module is NOT idempotent."
type: str
ignore_timestamps:
description:
- "Whether the I(valid_from) and I(valid_to) timestamps should be ignored for idempotency checks."
- "However, the values will still be applied to a new certificate if it meets any other necessary conditions for generation/regeneration."
type: bool
default: false
version_added: 2.2.0
principals:
description:
- "Certificates may be limited to be valid for a set of principal (user/host) names.
@@ -192,6 +204,7 @@ EXAMPLES = '''
valid_from: +0s
valid_to: +32w
valid_at: +2w
ignore_timestamps: true
- name: Generate an OpenSSH host certificate that is valid forever and only for example.com and examplehost
community.crypto.openssh_cert:
@@ -258,10 +271,11 @@ info:
'''
import os
from distutils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import (
KeygenCommand,
@@ -295,6 +309,7 @@ class Certificate(OpensshModule):
self.type = self.module.params['type']
self.use_agent = self.module.params['use_agent']
self.valid_at = self.module.params['valid_at']
self.ignore_timestamps = self.module.params['ignore_timestamps']
self._check_if_base_dir(self.path)
@@ -378,7 +393,7 @@ class Certificate(OpensshModule):
def _is_fully_valid(self):
return self._is_partially_valid() and all([
self._compare_options(),
self._compare_options() if self.original_data.type == 'user' else True,
self.original_data.key_id == self.identifier,
self.original_data.public_key == self._get_key_fingerprint(self.public_key),
self.original_data.signing_key == self._get_key_fingerprint(self.signing_key),
@@ -402,6 +417,9 @@ class Certificate(OpensshModule):
except ValueError as e:
return self.module.fail_json(msg=to_native(e))
if self.ignore_timestamps:
return original_time_parameters.within_range(self.valid_at)
return all([
original_time_parameters == self.time_parameters,
original_time_parameters.within_range(self.valid_at)
@@ -536,6 +554,7 @@ def main():
valid_at=dict(type='str'),
valid_from=dict(type='str'),
valid_to=dict(type='str'),
ignore_timestamps=dict(type='bool', default=False),
),
supports_check_mode=True,
add_file_common_args=True,

View File

@@ -94,7 +94,7 @@ options:
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
isn't matching, and will never regenerate an existing key.
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
@@ -178,7 +178,7 @@ public_key:
description: The public key of the generated SSH private key.
returned: changed or success
type: str
sample: ssh-rsa AAAAB3Nza(...omitted...)veL4E3Xcw== test_key
sample: ssh-rsa AAAAB3Nza(...omitted...)veL4E3Xcw==
comment:
description: The comment of the generated key.
returned: changed or success

View File

@@ -1 +0,0 @@
x509_certificate.py

View File

@@ -1 +0,0 @@
x509_certificate_info.py

View File

@@ -14,7 +14,7 @@ DOCUMENTATION = r'''
module: openssl_csr
short_description: Generate OpenSSL Certificate Signing Request (CSR)
description:
- "Please note that the module regenerates an existing CSR if it doesn't match the module's
- "Please note that the module regenerates an existing CSR if it does not match the module's
options, or if it seems to be corrupt. If you are concerned that this could overwrite
your existing CSR, consider using the I(backup) option."
author:

View File

@@ -17,13 +17,9 @@ description:
- This module allows one to query information on OpenSSL Certificate Signing Requests (CSR).
- In case the CSR signature cannot be validated, the module will fail. In this case, all return
variables are still returned.
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
and will be removed in community.crypto 2.0.0.
- It uses the cryptography python library to interact with OpenSSL.
requirements:
- PyOpenSSL >= 0.15 or cryptography >= 1.3
- cryptography >= 1.3
author:
- Felix Fontein (@felixfontein)
- Yanis Guenane (@Spredzy)
@@ -42,14 +38,14 @@ options:
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
From that point on, only the C(cryptography) backend will be available.
type: str
default: auto
choices: [ auto, cryptography, pyopenssl ]
choices: [ auto, cryptography ]
extends_documentation_fragment:
- community.crypto.name_encoding
seealso:
- module: community.crypto.openssl_csr
@@ -110,7 +106,13 @@ extensions_by_oid:
returned: success
type: bool
value:
description: The Base64 encoded value (in DER format) of the extension
description:
- The Base64 encoded value (in DER format) of the extension.
- B(Note) that depending on the C(cryptography) version used, it is
not possible to extract the ASN.1 content of the extension, but only
to provide the re-encoded content of the extension in case it was
parsed by C(cryptography). This should usually result in exactly the
same value, except if the original extension value was malformed.
returned: success
type: str
sample: "MAMCAQU="
@@ -125,7 +127,9 @@ key_usage_critical:
returned: success
type: bool
subject_alt_name:
description: Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
description:
- Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
- See I(name_encoding) for how IDNs are handled.
returned: success
type: list
elements: str
@@ -153,6 +157,7 @@ name_constraints_excluded:
description:
- List of excluded subtrees the CA cannot sign certificates for.
- Is C(none) if extension is not present.
- See I(name_encoding) for how IDNs are handled.
returned: success
type: list
elements: str
@@ -267,7 +272,7 @@ subject_key_identifier:
- The CSR's subject key identifier.
- The identifier is returned in hexadecimal, with C(:) used to separate bytes.
- Is C(none) if the C(SubjectKeyIdentifier) extension is not present.
returned: success and if the pyOpenSSL backend is I(not) used
returned: success
type: str
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
authority_key_identifier:
@@ -275,14 +280,15 @@ authority_key_identifier:
- The CSR's authority key identifier.
- The identifier is returned in hexadecimal, with C(:) used to separate bytes.
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
returned: success and if the pyOpenSSL backend is I(not) used
returned: success
type: str
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
authority_cert_issuer:
description:
- The CSR's authority cert issuer as a list of general names.
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
returned: success and if the pyOpenSSL backend is I(not) used
- See I(name_encoding) for how IDNs are handled.
returned: success
type: list
elements: str
sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
@@ -290,7 +296,7 @@ authority_cert_serial_number:
description:
- The CSR's authority cert serial number.
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
returned: success and if the pyOpenSSL backend is I(not) used
returned: success
type: int
sample: '12345'
'''
@@ -313,7 +319,8 @@ def main():
argument_spec=dict(
path=dict(type='path'),
content=dict(type='str'),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
),
required_one_of=(
['path', 'content'],

View File

@@ -15,7 +15,7 @@ module: openssl_csr_pipe
short_description: Generate OpenSSL Certificate Signing Request (CSR)
version_added: 1.3.0
description:
- "Please note that the module regenerates an existing CSR if it doesn't match the module's
- "Please note that the module regenerates an existing CSR if it does not match the module's
options, or if it seems to be corrupt."
author:
- Yanis Guenane (@Spredzy)

View File

@@ -67,7 +67,7 @@ options:
version_added: "1.0.0"
return_content:
description:
- If set to C(yes), will return the (current or generated) DH params' content as I(dhparams).
- If set to C(yes), will return the (current or generated) DH parameter's content as I(dhparams).
type: bool
default: no
version_added: "1.0.0"
@@ -128,11 +128,11 @@ import re
import tempfile
import traceback
from distutils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.io import (
load_file_if_exists,
write_file,
@@ -286,7 +286,7 @@ class DHParameterOpenSSL(DHParameterBase):
rc, out, err = module.run_command(command, check_rc=False)
result = to_native(out)
if rc != 0:
# If the call failed the file probably doesn't exist or is
# If the call failed the file probably does not exist or is
# unreadable
return False
# output contains "(xxxx bit)"
@@ -380,7 +380,7 @@ def main():
# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect either the required Python library cryptography (>= {0}) "
module.fail_json(msg=("Cannot detect either the required Python library cryptography (>= {0}) "
"or the OpenSSL binary openssl").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == 'openssl':

View File

@@ -92,7 +92,14 @@ options:
privatekey_path:
description:
- File to read private key from.
- Mutually exclusive with I(privatekey_content).
type: path
privatekey_content:
description:
- Content of the private key file.
- Mutually exclusive with I(privatekey_path).
type: str
version_added: "2.3.0"
state:
description:
- Whether the file should exist or not.
@@ -160,7 +167,7 @@ EXAMPLES = r'''
action: export
path: /opt/certs/ansible.p12
friendly_name: raclette
privatekey_path: /opt/certs/keys/key.pem
privatekey_content: '{{ private_key_contents }}'
certificate_path: /opt/certs/cert.pem
other_certificates_parse_all: true
other_certificates:
@@ -239,11 +246,11 @@ import os
import stat
import traceback
from distutils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.io import (
load_file_if_exists,
write_file,
@@ -276,7 +283,7 @@ try:
import OpenSSL
from OpenSSL import crypto
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
except ImportError:
except (ImportError, AttributeError):
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
@@ -328,6 +335,7 @@ class Pkcs(OpenSSLObject):
self.pkcs12 = None
self.privatekey_passphrase = module.params['privatekey_passphrase']
self.privatekey_path = module.params['privatekey_path']
self.privatekey_content = module.params['privatekey_content']
self.pkcs12_bytes = None
self.return_content = module.params['return_content']
self.src = module.params['src']
@@ -338,6 +346,15 @@ class Pkcs(OpenSSLObject):
self.backup = module.params['backup']
self.backup_file = None
if self.privatekey_path is not None:
try:
with open(self.privatekey_path, 'rb') as fh:
self.privatekey_content = fh.read()
except (IOError, OSError) as exc:
raise PkcsError(exc)
elif self.privatekey_content is not None:
self.privatekey_content = to_bytes(self.privatekey_content)
if self.other_certificates:
if self.other_certificates_parse_all:
filenames = list(self.other_certificates)
@@ -382,7 +399,7 @@ class Pkcs(OpenSSLObject):
def _check_pkey_passphrase():
if self.privatekey_passphrase:
try:
load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend)
load_privatekey(None, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend)
except OpenSSLObjectError:
return False
return True
@@ -397,11 +414,11 @@ class Pkcs(OpenSSLObject):
pkcs12_privatekey, pkcs12_certificate, pkcs12_other_certificates, pkcs12_friendly_name = self.parse()
except OpenSSLObjectError:
return False
if (pkcs12_privatekey is not None) and (self.privatekey_path is not None):
if (pkcs12_privatekey is not None) and (self.privatekey_content is not None):
expected_pkey = self._dump_privatekey(self.pkcs12)
if pkcs12_privatekey != expected_pkey:
return False
elif bool(pkcs12_privatekey) != bool(self.privatekey_path):
elif bool(pkcs12_privatekey) != bool(self.privatekey_content):
return False
if (pkcs12_certificate is not None) and (self.certificate_path is not None):
@@ -504,10 +521,10 @@ class PkcsPyOpenSSL(Pkcs):
if self.friendly_name:
self.pkcs12.set_friendlyname(to_bytes(self.friendly_name))
if self.privatekey_path:
if self.privatekey_content:
try:
self.pkcs12.set_privatekey(
load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend))
load_privatekey(None, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend))
except OpenSSLBadPassphraseError as exc:
raise PkcsError(exc)
@@ -558,9 +575,9 @@ class PkcsCryptography(Pkcs):
def generate_bytes(self, module):
"""Generate PKCS#12 file archive."""
pkey = None
if self.privatekey_path:
if self.privatekey_content:
try:
pkey = load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend)
pkey = load_privatekey(None, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend)
except OpenSSLBadPassphraseError as exc:
raise PkcsError(exc)
@@ -648,7 +665,7 @@ def select_backend(module, backend):
# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect any of the required Python libraries "
module.fail_json(msg=("Cannot detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION))
@@ -683,6 +700,7 @@ def main():
path=dict(type='path', required=True),
privatekey_passphrase=dict(type='str', no_log=True),
privatekey_path=dict(type='path'),
privatekey_content=dict(type='str', no_log=True),
state=dict(type='str', default='present', choices=['absent', 'present']),
src=dict(type='path'),
backup=dict(type='bool', default=False),
@@ -694,10 +712,15 @@ def main():
['action', 'parse', ['src']],
]
mutually_exclusive = [
['privatekey_path', 'privatekey_content'],
]
module = AnsibleModule(
add_file_common_args=True,
argument_spec=argument_spec,
required_if=required_if,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True,
)

View File

@@ -117,7 +117,6 @@ filename:
fingerprint:
description:
- The fingerprint of the public key. Fingerprint will be generated for each C(hashlib.algorithms) available.
- The PyOpenSSL backend requires PyOpenSSL >= 16.0 for meaningful output.
returned: changed or success
type: dict
sample:
@@ -143,7 +142,6 @@ privatekey:
import os
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.io import (

View File

@@ -0,0 +1,161 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2022, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: openssl_privatekey_convert
short_description: Convert OpenSSL private keys
version_added: 2.1.0
description:
- This module allows one to convert OpenSSL private keys.
- The default mode for the private key file will be C(0600) if I(mode) is not explicitly set.
author:
- Felix Fontein (@felixfontein)
options:
dest_path:
description:
- Name of the file in which the generated TLS/SSL private key will be written. It will have C(0600) mode
if I(mode) is not explicitly set.
type: path
required: true
backup:
description:
- Create a backup file including a timestamp so you can get
the original private key back if you overwrote it with a new one by accident.
type: bool
default: false
extends_documentation_fragment:
- ansible.builtin.files
- community.crypto.module_privatekey_convert
seealso: []
'''
EXAMPLES = r'''
- name: Convert private key to PKCS8 format with passphrase
community.crypto.openssl_privatekey_convert:
src_path: /etc/ssl/private/ansible.com.pem
dest_path: /etc/ssl/private/ansible.com.key
dest_passphrase: '{{ private_key_passphrase }}'
format: pkcs8
'''
RETURN = r'''
backup_file:
description: Name of backup file created.
returned: changed and if I(backup) is C(yes)
type: str
sample: /path/to/privatekey.pem.2019-03-09@11:22~
'''
import os
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.io import (
load_file_if_exists,
write_file,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
OpenSSLObject,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_convert import (
select_backend,
get_privatekey_argument_spec,
)
class PrivateKeyConvertModule(OpenSSLObject):
def __init__(self, module, module_backend):
super(PrivateKeyConvertModule, self).__init__(
module.params['dest_path'],
'present',
False,
module.check_mode,
)
self.module_backend = module_backend
self.backup = module.params['backup']
self.backup_file = None
module.params['path'] = module.params['dest_path']
if module.params['mode'] is None:
module.params['mode'] = '0600'
module_backend.set_existing_destination(load_file_if_exists(self.path, module))
def generate(self, module):
"""Do conversion."""
if self.module_backend.needs_conversion():
# Convert
privatekey_data = self.module_backend.get_private_key_data()
if not self.check_mode:
if self.backup:
self.backup_file = module.backup_local(self.path)
write_file(module, privatekey_data, 0o600)
self.changed = True
file_args = module.load_file_common_arguments(module.params)
if module.check_file_absent_if_check_mode(file_args['path']):
self.changed = True
else:
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
def dump(self):
"""Serialize the object into a dictionary."""
result = self.module_backend.dump()
result['changed'] = self.changed
if self.backup_file:
result['backup_file'] = self.backup_file
return result
def main():
argument_spec = get_privatekey_argument_spec()
argument_spec.argument_spec.update(dict(
dest_path=dict(type='path', required=True),
backup=dict(type='bool', default=False),
))
module = argument_spec.create_ansible_module(
supports_check_mode=True,
add_file_common_args=True,
)
base_dir = os.path.dirname(module.params['dest_path']) or '.'
if not os.path.isdir(base_dir):
module.fail_json(
name=base_dir,
msg='The directory %s does not exist or the file is not a directory' % base_dir
)
module_backend = select_backend(module=module)
try:
private_key = PrivateKeyConvertModule(module, module_backend)
private_key.generate(module)
result = private_key.dump()
module.exit_json(**result)
except OpenSSLObjectError as exc:
module.fail_json(msg=to_native(exc))
if __name__ == '__main__':
main()

View File

@@ -19,13 +19,9 @@ description:
private key. In this case, all return variables are still returned. Note that key consistency
checks are not available all key types; if none is available, C(none) is returned for
C(key_is_consistent).
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
and will be removed in community.crypto 2.0.0.
- It uses the cryptography python library to interact with OpenSSL.
requirements:
- PyOpenSSL >= 0.15 or cryptography >= 1.2.3
- cryptography >= 1.2.3
author:
- Felix Fontein (@felixfontein)
- Yanis Guenane (@Spredzy)
@@ -49,21 +45,29 @@ options:
- Whether to return private key data.
- Only set this to C(yes) when you want private information about this key to
leave the remote machine.
- "WARNING: you have to make sure that private key data isn't accidentally logged!"
- "B(WARNING:) you have to make sure that private key data is not accidentally logged!"
type: bool
default: no
check_consistency:
description:
- Whether to check consistency of the private key.
- In community.crypto < 2.0.0, consistency was always checked.
- Since community.crypto 2.0.0, the consistency check has been disabled by default to
avoid private key material to be transported around and computed with, and only do
so when requested explicitly. This can potentially prevent
L(side-channel attacks,https://en.wikipedia.org/wiki/Side-channel_attack).
type: bool
default: false
version_added: 2.0.0
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
From that point on, only the C(cryptography) backend will be available.
type: str
default: auto
choices: [ auto, cryptography, pyopenssl ]
choices: [ auto, cryptography ]
notes:
- Supports C(check_mode).
@@ -102,7 +106,7 @@ key_is_consistent:
- Whether the key is consistent. Can also return C(none) next to C(yes) and
C(no), to indicate that consistency could not be checked.
- In case the check returns C(no), the module will fail.
returned: always
returned: when I(check_consistency=true)
type: bool
public_key:
description: Private key's public key in PEM format.
@@ -215,7 +219,8 @@ def main():
content=dict(type='str', no_log=True),
passphrase=dict(type='str', no_log=True),
return_private_key_data=dict(type='bool', default=False),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
check_consistency=dict(type='bool', default=False),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
),
required_one_of=(
['path', 'content'],
@@ -248,7 +253,8 @@ def main():
module.params['select_crypto_backend'],
data,
passphrase=module.params['passphrase'],
return_private_key_data=module.params['return_private_key_data'])
return_private_key_data=module.params['return_private_key_data'],
check_consistency=module.params['check_consistency'])
try:
result.update(module_backend.get_info())

View File

@@ -97,7 +97,6 @@ curve:
fingerprint:
description:
- The fingerprint of the public key. Fingerprint will be generated for each C(hashlib.algorithms) available.
- The PyOpenSSL backend requires PyOpenSSL >= 16.0 for meaningful output.
returned: changed or success
type: dict
sample:

View File

@@ -15,14 +15,9 @@ short_description: Generate an OpenSSL public key from its private key.
description:
- This module allows one to (re)generate OpenSSL public keys from their private keys.
- Keys are generated in PEM or OpenSSH format.
- "The module can use the cryptography Python library, or the pyOpenSSL Python
library. By default, it tries to detect which one is available. This can be
overridden with the I(select_crypto_backend) option. When I(format) is C(OpenSSH),
the C(cryptography) backend has to be used. Please note that the PyOpenSSL backend
was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
- The module uses the cryptography Python library.
requirements:
- Either cryptography >= 1.2.3 (older versions might work as well)
- Or pyOpenSSL >= 16.0.0
- cryptography >= 1.2.3 (older versions might work as well)
- Needs cryptography >= 1.4 if I(format) is C(OpenSSH)
author:
- Yanis Guenane (@Spredzy)
@@ -76,12 +71,11 @@ options:
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [ auto, cryptography, pyopenssl ]
choices: [ auto, cryptography ]
return_content:
description:
- If set to C(yes), will return the (current or generated) public key's content as I(publickey).
@@ -157,7 +151,6 @@ filename:
fingerprint:
description:
- The fingerprint of the public key. Fingerprint will be generated for each hashlib.algorithms available.
- Requires PyOpenSSL >= 16.0 for meaningful output.
returned: changed or success
type: dict
sample:
@@ -182,11 +175,11 @@ publickey:
import os
import traceback
from distutils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.io import (
load_file_if_exists,
write_file,
@@ -208,21 +201,9 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
get_publickey_info,
)
MINIMAL_PYOPENSSL_VERSION = '16.0.0'
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH = '1.4'
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
except ImportError:
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
PYOPENSSL_FOUND = True
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
@@ -300,11 +281,6 @@ class PublicKey(OpenSSLObject):
crypto_serialization.Encoding.PEM,
crypto_serialization.PublicFormat.SubjectPublicKeyInfo
)
else:
try:
return crypto.dump_publickey(crypto.FILETYPE_PEM, self.privatekey)
except AttributeError as dummy:
raise PublicKeyError('You need to have PyOpenSSL>=16.0.0 to generate public keys')
def generate(self, module):
"""Generate the public key."""
@@ -372,11 +348,6 @@ class PublicKey(OpenSSLObject):
crypto_serialization.Encoding.PEM,
crypto_serialization.PublicFormat.SubjectPublicKeyInfo
)
else:
publickey_content = crypto.dump_publickey(
crypto.FILETYPE_PEM,
crypto.load_publickey(crypto.FILETYPE_PEM, publickey_content)
)
except Exception as dummy:
return False
@@ -434,7 +405,7 @@ def main():
format=dict(type='str', default='PEM', choices=['OpenSSH', 'PEM']),
privatekey_passphrase=dict(type='str', no_log=True),
backup=dict(type='bool', default=False),
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
return_content=dict(type='bool', default=False),
),
supports_check_mode=True,
@@ -453,36 +424,20 @@ def main():
if backend == 'auto':
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(minimal_cryptography_version)
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
# Decision
if can_use_cryptography:
backend = 'cryptography'
elif can_use_pyopenssl:
if module.params['format'] == 'OpenSSH':
module.fail_json(
msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH)),
exception=CRYPTOGRAPHY_IMP_ERR
)
backend = 'pyopenssl'
# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
minimal_cryptography_version,
MINIMAL_PYOPENSSL_VERSION))
module.fail_json(msg=("Cannot detect the required Python library "
"cryptography (>= {0})").format(minimal_cryptography_version))
if module.params['format'] == 'OpenSSH' and backend != 'cryptography':
module.fail_json(msg="Format OpenSSH requires the cryptography backend.")
if backend == 'pyopenssl':
if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR)
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.0.0', collection_name='community.crypto')
elif backend == 'cryptography':
if backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(minimal_cryptography_version)),
exception=CRYPTOGRAPHY_IMP_ERR)

View File

@@ -14,14 +14,10 @@ module: openssl_publickey_info
short_description: Provide information for OpenSSL public keys
description:
- This module allows one to query information on OpenSSL public keys.
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
and will be removed in community.crypto 2.0.0.
- It uses the cryptography python library to interact with OpenSSL.
version_added: 1.7.0
requirements:
- PyOpenSSL >= 0.15 or cryptography >= 1.2.3
- cryptography >= 1.2.3
author:
- Felix Fontein (@felixfontein)
options:
@@ -38,14 +34,11 @@ options:
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
From that point on, only the C(cryptography) backend will be available.
type: str
default: auto
choices: [ auto, cryptography, pyopenssl ]
choices: [ auto, cryptography ]
notes:
- Supports C(check_mode).
@@ -61,7 +54,7 @@ EXAMPLES = r'''
path: /etc/ssl/private/ansible.com.pem
- name: Create public key from private key
community.crypto.openssl_privatekey:
community.crypto.openssl_publickey:
privatekey_path: /etc/ssl/private/ansible.com.pem
path: /etc/ssl/ansible.com.pub
@@ -174,7 +167,7 @@ def main():
argument_spec=dict(
path=dict(type='path'),
content=dict(type='str', no_log=True),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
),
required_one_of=(
['path', 'content'],

View File

@@ -15,13 +15,9 @@ version_added: 1.1.0
short_description: Sign data with openssl
description:
- This module allows one to sign data using a private key.
- The module can use the cryptography Python library, or the pyOpenSSL Python
library. By default, it tries to detect which one is available. This can be
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL backend
was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0.
- The module uses the cryptography Python library.
requirements:
- Either cryptography >= 1.4 (some key types require newer versions)
- Or pyOpenSSL >= 0.11 (Ed25519 and Ed448 keys are not supported with this backend)
- cryptography >= 1.4 (some key types require newer versions)
author:
- Patrick Pichler (@aveexy)
- Markus Teufelberger (@MarkusTeufelberger)
@@ -50,12 +46,11 @@ options:
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [ auto, cryptography, pyopenssl ]
choices: [ auto, cryptography ]
notes:
- |
When using the C(cryptography) backend, the following key types require at least the following C(cryptography) version:
@@ -96,22 +91,11 @@ signature:
import os
import traceback
from distutils.version import LooseVersion
import base64
MINIMAL_PYOPENSSL_VERSION = '0.11'
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
except ImportError:
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
PYOPENSSL_FOUND = True
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
CRYPTOGRAPHY_IMP_ERR = None
try:
@@ -139,7 +123,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
load_privatekey,
)
from ansible.module_utils.common.text.converters import to_native, to_bytes
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
@@ -170,34 +154,6 @@ class SignatureBase(OpenSSLObject):
pass
# Implementation with using pyOpenSSL
class SignaturePyOpenSSL(SignatureBase):
def __init__(self, module, backend):
super(SignaturePyOpenSSL, self).__init__(module, backend)
def run(self):
result = dict()
try:
with open(self.path, "rb") as f:
_in = f.read()
private_key = load_privatekey(
path=self.privatekey_path,
content=self.privatekey_content,
passphrase=self.privatekey_passphrase,
backend=self.backend,
)
signature = OpenSSL.crypto.sign(private_key, _in, "sha256")
result['signature'] = base64.b64encode(signature)
return result
except Exception as e:
raise OpenSSLObjectError(e)
# Implementation with using cryptography
class SignatureCryptography(SignatureBase):
@@ -262,7 +218,7 @@ def main():
privatekey_content=dict(type='str', no_log=True),
privatekey_passphrase=dict(type='str', no_log=True),
path=dict(type='path', required=True),
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
),
mutually_exclusive=(
['privatekey_path', 'privatekey_content'],
@@ -283,29 +239,17 @@ def main():
if backend == 'auto':
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
# Decision
if can_use_cryptography:
backend = 'cryptography'
elif can_use_pyopenssl:
backend = 'pyopenssl'
# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION))
module.fail_json(msg=("Cannot detect the required Python library "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
try:
if backend == 'pyopenssl':
if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR)
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.0.0', collection_name='community.crypto')
_sign = SignaturePyOpenSSL(module, backend)
elif backend == 'cryptography':
if backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)

View File

@@ -15,13 +15,9 @@ version_added: 1.1.0
short_description: Verify signatures with openssl
description:
- This module allows one to verify a signature for a file by a certificate.
- The module can use the cryptography Python library, or the pyOpenSSL Python
library. By default, it tries to detect which one is available. This can be
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL backend
was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0.
- The module uses the cryptography Python library.
requirements:
- Either cryptography >= 1.4 (some key types require newer versions)
- Or pyOpenSSL >= 0.11 (Ed25519 and Ed448 keys are not supported with this backend)
- cryptography >= 1.4 (some key types require newer versions)
author:
- Patrick Pichler (@aveexy)
- Markus Teufelberger (@MarkusTeufelberger)
@@ -49,12 +45,11 @@ options:
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [ auto, cryptography, pyopenssl ]
choices: [ auto, cryptography ]
notes:
- |
When using the C(cryptography) backend, the following key types require at least the following C(cryptography) version:
@@ -96,22 +91,11 @@ valid:
import os
import traceback
from distutils.version import LooseVersion
import base64
MINIMAL_PYOPENSSL_VERSION = '0.11'
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
except ImportError:
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
PYOPENSSL_FOUND = True
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
CRYPTOGRAPHY_IMP_ERR = None
try:
@@ -139,7 +123,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
load_certificate,
)
from ansible.module_utils.common.text.converters import to_native, to_bytes
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
@@ -170,37 +154,6 @@ class SignatureInfoBase(OpenSSLObject):
pass
# Implementation with using pyOpenSSL
class SignatureInfoPyOpenSSL(SignatureInfoBase):
def __init__(self, module, backend):
super(SignatureInfoPyOpenSSL, self).__init__(module, backend)
def run(self):
result = dict()
try:
with open(self.path, "rb") as f:
_in = f.read()
_signature = base64.b64decode(self.signature)
certificate = load_certificate(
path=self.certificate_path,
content=self.certificate_content,
backend=self.backend,
)
try:
OpenSSL.crypto.verify(certificate, _signature, _in, 'sha256')
result['valid'] = True
except Exception:
result['valid'] = False
return result
except Exception as e:
raise OpenSSLObjectError(e)
# Implementation with using cryptography
class SignatureInfoCryptography(SignatureInfoBase):
@@ -295,7 +248,7 @@ def main():
certificate_content=dict(type='str'),
path=dict(type='path', required=True),
signature=dict(type='str', required=True),
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
),
mutually_exclusive=(
['certificate_path', 'certificate_content'],
@@ -316,29 +269,17 @@ def main():
if backend == 'auto':
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
# Decision
if can_use_cryptography:
backend = 'cryptography'
elif can_use_pyopenssl:
backend = 'pyopenssl'
# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION))
module.fail_json(msg=("Cannot detect any of the required Python libraries "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
try:
if backend == 'pyopenssl':
if not PYOPENSSL_FOUND:
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
exception=PYOPENSSL_IMP_ERR)
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
version='2.0.0', collection_name='community.crypto')
_sign = SignatureInfoPyOpenSSL(module, backend)
elif backend == 'cryptography':
if backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)

View File

@@ -14,7 +14,7 @@ DOCUMENTATION = r'''
module: x509_certificate
short_description: Generate and/or check OpenSSL certificates
description:
- It implements a notion of provider (ie. C(selfsigned), C(ownca), C(acme), C(assertonly), C(entrust))
- It implements a notion of provider (one of C(selfsigned), C(ownca), C(acme), and C(entrust))
for your certificate.
- "Please note that the module regenerates existing certificate if it does not match the module's
options, or if it seems to be corrupt. If you are concerned that this could overwrite
@@ -47,8 +47,6 @@ options:
provider:
description:
- Name of the provider to use to generate/retrieve the OpenSSL certificate.
- The C(assertonly) provider will not generate files and fail if the certificate file is missing.
- The C(assertonly) provider has been deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0.
Please see the examples on how to emulate it with
M(community.crypto.x509_certificate_info), M(community.crypto.openssl_csr_info),
M(community.crypto.openssl_privatekey_info) and M(ansible.builtin.assert).
@@ -56,7 +54,7 @@ options:
L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API."
- Required if I(state) is C(present).
type: str
choices: [ acme, assertonly, entrust, ownca, selfsigned ]
choices: [ acme, entrust, ownca, selfsigned ]
return_content:
description:
@@ -69,9 +67,6 @@ options:
description:
- Create a backup file including a timestamp so you can get the original
certificate back if you overwrote it with a new one by accident.
- This is not used by the C(assertonly) provider.
- This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in community.crypto 2.0.0.
For alternatives, see the example on replacing C(assertonly).
type: bool
default: no
@@ -96,7 +91,6 @@ extends_documentation_fragment:
- ansible.builtin.files
- community.crypto.module_certificate
- community.crypto.module_certificate.backend_acme_documentation
- community.crypto.module_certificate.backend_assertonly_documentation
- community.crypto.module_certificate.backend_entrust_documentation
- community.crypto.module_certificate.backend_ownca_documentation
- community.crypto.module_certificate.backend_selfsigned_documentation
@@ -150,40 +144,9 @@ EXAMPLES = r'''
entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-key.crt
entrust_api_specification_path: /etc/ssl/entrust/api-docs/cms-api-2.1.0.yaml
# The following example shows one assertonly usage using all existing options for
# assertonly, and shows how to emulate the behavior with the x509_certificate_info,
# openssl_csr_info, openssl_privatekey_info and assert modules:
- name: Usage of assertonly with all existing options
community.crypto.x509_certificate:
provider: assertonly
path: /etc/ssl/crt/ansible.com.crt
csr_path: /etc/ssl/csr/ansible.com.csr
privatekey_path: /etc/ssl/csr/ansible.com.key
signature_algorithms:
- sha256WithRSAEncryption
- sha512WithRSAEncryption
subject:
commonName: ansible.com
subject_strict: yes
issuer:
commonName: ansible.com
issuer_strict: yes
has_expired: no
version: 3
key_usage:
- Data Encipherment
key_usage_strict: yes
extended_key_usage:
- DVCS
extended_key_usage_strict: yes
subject_alt_name:
- dns:ansible.com
subject_alt_name_strict: yes
not_before: 20190331202428Z
not_after: 20190413202428Z
valid_at: "+1d10h"
invalid_at: 20200331202428Z
valid_in: 10 # in ten seconds
# The following example shows how to emulate the behavior of the removed
# "assertonly" provider with the x509_certificate_info, openssl_csr_info,
# openssl_privatekey_info and assert modules:
- name: Get certificate information
community.crypto.x509_certificate_info:
@@ -208,9 +171,9 @@ EXAMPLES = r'''
- assert:
that:
# When private key is specified for assertonly, this will be checked:
# When private key was specified for assertonly, this was checked:
- result.public_key == result_privatekey.public_key
# When CSR is specified for assertonly, this will be checked:
# When CSR was specified for assertonly, this was checked:
- result.public_key == result_csr.public_key
- result.subject_ordered == result_csr.subject_ordered
- result.extensions_by_oid == result_csr.extensions_by_oid
@@ -242,103 +205,6 @@ EXAMPLES = r'''
- "result.valid_at.one_day_ten_hours" # for valid_at
- "not result.valid_at.fixed_timestamp" # for invalid_at
- "result.valid_at.ten_seconds" # for valid_in
# Examples for some checks one could use the assertonly provider for:
# (Please note that assertonly has been deprecated!)
# How to use the assertonly provider to implement and trigger your own custom certificate generation workflow:
- name: Check if a certificate is currently still valid, ignoring failures
community.crypto.x509_certificate:
path: /etc/ssl/crt/example.com.crt
provider: assertonly
has_expired: no
ignore_errors: yes
register: validity_check
- name: Run custom task(s) to get a new, valid certificate in case the initial check failed
command: superspecialSSL recreate /etc/ssl/crt/example.com.crt
when: validity_check.failed
- name: Check the new certificate again for validity with the same parameters, this time failing the play if it is still invalid
community.crypto.x509_certificate:
path: /etc/ssl/crt/example.com.crt
provider: assertonly
has_expired: no
when: validity_check.failed
# Some other checks that assertonly could be used for:
- name: Verify that an existing certificate was issued by the Let's Encrypt CA and is currently still valid
community.crypto.x509_certificate:
path: /etc/ssl/crt/example.com.crt
provider: assertonly
issuer:
O: Let's Encrypt
has_expired: no
- name: Ensure that a certificate uses a modern signature algorithm (no SHA1, MD5 or DSA)
community.crypto.x509_certificate:
path: /etc/ssl/crt/example.com.crt
provider: assertonly
signature_algorithms:
- sha224WithRSAEncryption
- sha256WithRSAEncryption
- sha384WithRSAEncryption
- sha512WithRSAEncryption
- sha224WithECDSAEncryption
- sha256WithECDSAEncryption
- sha384WithECDSAEncryption
- sha512WithECDSAEncryption
- name: Ensure that the existing certificate belongs to the specified private key
community.crypto.x509_certificate:
path: /etc/ssl/crt/example.com.crt
privatekey_path: /etc/ssl/private/example.com.pem
provider: assertonly
- name: Ensure that the existing certificate is still valid at the winter solstice 2017
community.crypto.x509_certificate:
path: /etc/ssl/crt/example.com.crt
provider: assertonly
valid_at: 20171221162800Z
- name: Ensure that the existing certificate is still valid 2 weeks (1209600 seconds) from now
community.crypto.x509_certificate:
path: /etc/ssl/crt/example.com.crt
provider: assertonly
valid_in: 1209600
- name: Ensure that the existing certificate is only used for digital signatures and encrypting other keys
community.crypto.x509_certificate:
path: /etc/ssl/crt/example.com.crt
provider: assertonly
key_usage:
- digitalSignature
- keyEncipherment
key_usage_strict: true
- name: Ensure that the existing certificate can be used for client authentication
community.crypto.x509_certificate:
path: /etc/ssl/crt/example.com.crt
provider: assertonly
extended_key_usage:
- clientAuth
- name: Ensure that the existing certificate can only be used for client authentication and time stamping
community.crypto.x509_certificate:
path: /etc/ssl/crt/example.com.crt
provider: assertonly
extended_key_usage:
- clientAuth
- 1.3.6.1.5.5.7.3.8
extended_key_usage_strict: true
- name: Ensure that the existing certificate has a certain domain in its subjectAltName
community.crypto.x509_certificate:
path: /etc/ssl/crt/example.com.crt
provider: assertonly
subject_alt_name:
- www.example.com
- test.example.com
'''
RETURN = r'''
@@ -374,11 +240,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
add_acme_provider_to_argument_spec,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_assertonly import (
AssertOnlyCertificateProvider,
add_assertonly_provider_to_argument_spec,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_entrust import (
EntrustCertificateProvider,
add_entrust_provider_to_argument_spec,
@@ -495,7 +356,6 @@ class GenericCertificate(OpenSSLObject):
def main():
argument_spec = get_certificate_argument_spec()
add_acme_provider_to_argument_spec(argument_spec)
add_assertonly_provider_to_argument_spec(argument_spec)
add_entrust_provider_to_argument_spec(argument_spec)
add_ownca_provider_to_argument_spec(argument_spec)
add_selfsigned_provider_to_argument_spec(argument_spec)
@@ -511,10 +371,6 @@ def main():
supports_check_mode=True,
)
if module._name == 'community.crypto.openssl_certificate':
module.deprecate("The 'community.crypto.openssl_certificate' module has been renamed to 'community.crypto.x509_certificate'",
version='2.0.0', collection_name='community.crypto')
try:
if module.params['state'] == 'absent':
certificate = CertificateAbsent(module)
@@ -537,7 +393,6 @@ def main():
provider = module.params['provider']
provider_map = {
'acme': AcmeCertificateProvider,
'assertonly': AssertOnlyCertificateProvider,
'entrust': EntrustCertificateProvider,
'ownca': OwnCACertificateProvider,
'selfsigned': SelfSignedCertificateProvider,

View File

@@ -15,11 +15,7 @@ module: x509_certificate_info
short_description: Provide information of OpenSSL X.509 certificates
description:
- This module allows one to query information on OpenSSL certificates.
- It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the
cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements)
cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with
C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9
and will be removed in community.crypto 2.0.0.
- It uses the cryptography python library to interact with OpenSSL.
- Note that this module was called C(openssl_certificate_info) when included directly in Ansible
up to version 2.9. When moved to the collection C(community.crypto), it was renamed to
M(community.crypto.x509_certificate_info). From Ansible 2.10 on, it can still be used by the
@@ -29,7 +25,7 @@ description:
keyword, the new name M(community.crypto.x509_certificate_info) should be used to avoid
a deprecation warning.
requirements:
- PyOpenSSL >= 0.15 or cryptography >= 1.6
- cryptography >= 1.6
author:
- Felix Fontein (@felixfontein)
- Yanis Guenane (@Spredzy)
@@ -54,20 +50,20 @@ options:
- 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]) (e.g. C(+32w1d2h), and ASN.1 TIME (in other words, pattern C(YYYYMMDDHHMMSSZ)).
+ C([w | d | h | m | s]) (for example C(+32w1d2h)), and ASN.1 TIME (in other words, pattern C(YYYYMMDDHHMMSSZ)).
Note that all timestamps will be treated as being in UTC.
type: dict
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
- If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
- The default choice is C(auto), which tries to use C(cryptography) if available.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
- Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in community.crypto 2.0.0.
From that point on, only the C(cryptography) backend will be available.
type: str
default: auto
choices: [ auto, cryptography, pyopenssl ]
choices: [ auto, cryptography ]
extends_documentation_fragment:
- community.crypto.name_encoding
notes:
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
@@ -154,7 +150,13 @@ extensions_by_oid:
returned: success
type: bool
value:
description: The Base64 encoded value (in DER format) of the extension.
description:
- The Base64 encoded value (in DER format) of the extension.
- B(Note) that depending on the C(cryptography) version used, it is
not possible to extract the ASN.1 content of the extension, but only
to provide the re-encoded content of the extension in case it was
parsed by C(cryptography). This should usually result in exactly the
same value, except if the original extension value was malformed.
returned: success
type: str
sample: "MAMCAQU="
@@ -169,7 +171,9 @@ key_usage_critical:
returned: success
type: bool
subject_alt_name:
description: Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
description:
- Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
- See I(name_encoding) for how IDNs are handled.
returned: success
type: list
elements: str
@@ -341,7 +345,7 @@ subject_key_identifier:
- The certificate's subject key identifier.
- The identifier is returned in hexadecimal, with C(:) used to separate bytes.
- Is C(none) if the C(SubjectKeyIdentifier) extension is not present.
returned: success and if the pyOpenSSL backend is I(not) used
returned: success
type: str
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
authority_key_identifier:
@@ -349,14 +353,15 @@ authority_key_identifier:
- The certificate's authority key identifier.
- The identifier is returned in hexadecimal, with C(:) used to separate bytes.
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
returned: success and if the pyOpenSSL backend is I(not) used
returned: success
type: str
sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
authority_cert_issuer:
description:
- The certificate's authority cert issuer as a list of general names.
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
returned: success and if the pyOpenSSL backend is I(not) used
- See I(name_encoding) for how IDNs are handled.
returned: success
type: list
elements: str
sample: "[DNS:www.ansible.com, IP:1.2.3.4]"
@@ -364,7 +369,7 @@ authority_cert_serial_number:
description:
- The certificate's authority cert serial number.
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
returned: success and if the pyOpenSSL backend is I(not) used
returned: success
type: int
sample: '12345'
ocsp_uri:
@@ -398,7 +403,8 @@ def main():
path=dict(type='path'),
content=dict(type='str'),
valid_at=dict(type='dict'),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
),
required_one_of=(
['path', 'content'],
@@ -408,9 +414,6 @@ def main():
),
supports_check_mode=True,
)
if module._name == 'community.crypto.openssl_certificate_info':
module.deprecate("The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_certificate_info'",
version='2.0.0', collection_name='community.crypto')
if module.params['content'] is not None:
data = module.params['content'].encode('utf-8')

View File

@@ -123,8 +123,6 @@ certificate:
'''
import os
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (

View File

@@ -92,8 +92,21 @@ options:
description:
- Key/value pairs that will be present in the issuer name field of the CRL.
- If you need to specify more than one value with the same key, use a list as value.
- Required if I(state) is C(present).
- If the order of the components is important, use I(issuer_ordered).
- One of I(issuer) and I(issuer_ordered) is required if I(state) is C(present).
- Mutually exclusive with I(issuer_ordered).
type: dict
issuer_ordered:
description:
- A list of dictionaries, where every dictionary must contain one key/value pair.
This key/value pair will be present in the issuer name field of the CRL.
- If you want to specify more than one value with the same key in a row, you can
use a list as value.
- One of I(issuer) and I(issuer_ordered) is required if I(state) is C(present).
- Mutually exclusive with I(issuer).
type: list
elements: dict
version_added: 2.0.0
last_update:
description:
@@ -101,7 +114,7 @@ options:
- 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]) (e.g. C(+32w1d2h).
+ C([w | d | h | m | s]) (for example C(+32w1d2h)).
- Note that if using relative time this module is NOT idempotent, except when
I(ignore_timestamps) is set to C(yes).
type: str
@@ -114,7 +127,7 @@ options:
- 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]) (e.g. C(+32w1d2h).
+ C([w | d | h | m | s]) (for example C(+32w1d2h)).
- Note that if using relative time this module is NOT idempotent, except when
I(ignore_timestamps) is set to C(yes).
- Required if I(state) is C(present).
@@ -159,7 +172,7 @@ options:
- 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]) (e.g. C(+32w1d2h).
+ C([w | d | h | m | s]) (for example C(+32w1d2h)).
- Note that if using relative time this module is NOT idempotent, except when
I(ignore_timestamps) is set to C(yes).
type: str
@@ -202,7 +215,7 @@ options:
- 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]) (e.g. C(+32w1d2h).
+ C([w | d | h | m | s]) (for example C(+32w1d2h)).
- Note that if using relative time this module is NOT idempotent. This will NOT
change when I(ignore_timestamps) is set to C(yes).
type: str
@@ -229,6 +242,7 @@ options:
extends_documentation_fragment:
- files
- community.crypto.name_encoding
notes:
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
@@ -284,6 +298,7 @@ issuer:
description:
- The CRL's issuer.
- Note that for repeated values, only the last one will be returned.
- See I(name_encoding) for how IDNs are handled.
returned: success
type: dict
sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
@@ -323,7 +338,9 @@ revoked_certificates:
type: str
sample: 20190413202428Z
issuer:
description: The certificate's issuer.
description:
- The certificate's issuer.
- See I(name_encoding) for how IDNs are handled.
type: list
elements: str
sample: '["DNS:ca.example.org"]'
@@ -367,11 +384,11 @@ import base64
import os
import traceback
from distutils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.io import (
write_file,
)
@@ -386,11 +403,13 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
load_privatekey,
load_certificate,
parse_name_field,
parse_ordered_name_field,
get_relative_time_option,
select_message_digest,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
cryptography_decode_name,
cryptography_get_name,
cryptography_name_to_oid,
cryptography_oid_to_name,
@@ -454,6 +473,7 @@ class CRL(OpenSSLObject):
self.update = module.params['mode'] == 'update'
self.ignore_timestamps = module.params['ignore_timestamps']
self.return_content = module.params['return_content']
self.name_encoding = module.params['name_encoding']
self.crl_content = None
self.privatekey_path = module.params['privatekey_path']
@@ -462,8 +482,15 @@ class CRL(OpenSSLObject):
self.privatekey_content = self.privatekey_content.encode('utf-8')
self.privatekey_passphrase = module.params['privatekey_passphrase']
self.issuer = parse_name_field(module.params['issuer'])
self.issuer = [(entry[0], entry[1]) for entry in self.issuer if entry[1]]
try:
if module.params['issuer_ordered']:
self.issuer_ordered = True
self.issuer = parse_ordered_name_field(module.params['issuer_ordered'], 'issuer_ordered')
else:
self.issuer_ordered = False
self.issuer = parse_name_field(module.params['issuer'], 'issuer')
except (TypeError, ValueError) as exc:
module.fail_json(msg=to_native(exc))
self.last_update = get_relative_time_option(module.params['last_update'], 'last_update')
self.next_update = get_relative_time_option(module.params['next_update'], 'next_update')
@@ -574,11 +601,20 @@ class CRL(OpenSSLObject):
super(CRL, self).remove(self.module)
def _compress_entry(self, entry):
issuer = None
if entry['issuer'] is not None:
# Normalize to IDNA. If this is used-provided, it was already converted to
# IDNA (by cryptography_get_name) and thus the `idna` library is present.
# If this is coming from cryptography and isn't already in IDNA (i.e. ascii),
# cryptography < 2.1 must be in use, which depends on `idna`. So this should
# not require `idna` except if it was already used by code earlier during
# this invocation.
issuer = tuple(cryptography_decode_name(issuer, idn_rewrite='idna') for issuer in entry['issuer'])
if self.ignore_timestamps:
# Throw out revocation_date
return (
entry['serial_number'],
tuple(entry['issuer']) if entry['issuer'] is not None else None,
issuer,
entry['issuer_critical'],
entry['reason'],
entry['reason_critical'],
@@ -589,7 +625,7 @@ class CRL(OpenSSLObject):
return (
entry['serial_number'],
entry['revocation_date'],
tuple(entry['issuer']) if entry['issuer'] is not None else None,
issuer,
entry['issuer_critical'],
entry['reason'],
entry['reason_critical'],
@@ -616,13 +652,17 @@ class CRL(OpenSSLObject):
return False
want_issuer = [(cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer]
if want_issuer != [(sub.oid, sub.value) for sub in self.crl.issuer]:
is_issuer = [(sub.oid, sub.value) for sub in self.crl.issuer]
if not self.issuer_ordered:
want_issuer = set(want_issuer)
is_issuer = set(is_issuer)
if want_issuer != is_issuer:
return False
old_entries = [self._compress_entry(cryptography_decode_revoked_certificate(cert)) for cert in self.crl]
new_entries = [self._compress_entry(cert) for cert in self.revoked_certificates]
if self.update:
# We don't simply use a set so that duplicate entries are treated correctly
# We do not simply use a set so that duplicate entries are treated correctly
for entry in new_entries:
try:
old_entries.remove(entry)
@@ -664,9 +704,7 @@ class CRL(OpenSSLObject):
revoked_cert = revoked_cert.revocation_date(entry['revocation_date'])
if entry['issuer'] is not None:
revoked_cert = revoked_cert.add_extension(
x509.CertificateIssuer([
cryptography_get_name(name, 'issuer') for name in entry['issuer']
]),
x509.CertificateIssuer(entry['issuer']),
entry['issuer_critical']
)
if entry['reason'] is not None:
@@ -742,7 +780,7 @@ class CRL(OpenSSLObject):
result['issuer'][k] = v
result['revoked_certificates'] = []
for entry in self.revoked_certificates:
result['revoked_certificates'].append(cryptography_dump_revoked(entry))
result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding))
elif self.crl:
result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
@@ -757,7 +795,7 @@ class CRL(OpenSSLObject):
result['revoked_certificates'] = []
for cert in self.crl:
entry = cryptography_decode_revoked_certificate(cert)
result['revoked_certificates'].append(cryptography_dump_revoked(entry))
result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding))
if self.return_content:
result['crl'] = self.crl_content
@@ -782,6 +820,7 @@ def main():
privatekey_content=dict(type='str', no_log=True),
privatekey_passphrase=dict(type='str', no_log=True),
issuer=dict(type='dict'),
issuer_ordered=dict(type='list', elements='dict'),
last_update=dict(type='str', default='+0s'),
next_update=dict(type='str'),
digest=dict(type='str', default='sha256'),
@@ -812,13 +851,16 @@ def main():
required_one_of=[['path', 'content', 'serial_number']],
mutually_exclusive=[['path', 'content', 'serial_number']],
),
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
),
required_if=[
('state', 'present', ['privatekey_path', 'privatekey_content'], True),
('state', 'present', ['issuer', 'next_update', 'revoked_certificates'], False),
('state', 'present', ['issuer', 'issuer_ordered'], True),
('state', 'present', ['next_update', 'revoked_certificates'], False),
],
mutually_exclusive=(
['privatekey_path', 'privatekey_content'],
['issuer', 'issuer_ordered'],
),
supports_check_mode=True,
add_file_common_args=True,

View File

@@ -40,6 +40,9 @@ options:
default: true
version_added: 1.7.0
extends_documentation_fragment:
- community.crypto.name_encoding
notes:
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
They are all in UTC.
@@ -76,6 +79,7 @@ issuer:
description:
- The CRL's issuer.
- Note that for repeated values, only the last one will be returned.
- See I(name_encoding) for how IDNs are handled.
returned: success
type: dict
sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
@@ -115,7 +119,9 @@ revoked_certificates:
type: str
sample: 20190413202428Z
issuer:
description: The certificate's issuer.
description:
- The certificate's issuer.
- See I(name_encoding) for how IDNs are handled.
type: list
elements: str
sample: '["DNS:ca.example.org"]'
@@ -173,6 +179,7 @@ def main():
path=dict(type='path'),
content=dict(type='str'),
list_revoked_certificates=dict(type='bool', default=True),
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
),
required_one_of=(
['path', 'content'],

View File

@@ -166,7 +166,7 @@ class AnsibleActionModule(object):
try:
self.aliases = self._handle_aliases()
except (ValueError, TypeError) as e:
# Use exceptions here because it isn't safe to call fail_json until no_log is processed
# Use exceptions here because it is not safe to call fail_json until no_log is processed
raise _ModuleExitException(dict(failed=True, msg="Module alias error: %s" % to_native(e)))
# Save parameter values that should never be logged
@@ -405,7 +405,7 @@ class AnsibleActionModule(object):
self.fail_json(msg=msg)
elif param[k] not in choices:
# PyYaml converts certain strings to bools. If we can unambiguously convert back, do so before checking
# the value. If we can't figure this out, module author is responsible.
# the value. If we cannot figure this out, module author is responsible.
lowered_choices = None
if param[k] == 'False':
lowered_choices = lenient_lowercase(choices)
@@ -545,7 +545,7 @@ class AnsibleActionModule(object):
except KeyError:
self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k))
else:
# set the type_checker to the callable, and reset wanted to the callable's name (or type if it doesn't have one, ala MagicMock)
# set the type_checker to the callable, and reset wanted to the callable's name (or type if it does not have one, ala MagicMock)
type_checker = wanted
wanted = getattr(wanted, '__name__', to_native(type(wanted)))

8
simplified_bsd.txt Normal file
View File

@@ -0,0 +1,8 @@
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

34
tests/ee/all.yml Normal file
View File

@@ -0,0 +1,34 @@
- hosts: localhost
tasks:
- name: Register cryptography version
command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'"
register: cryptography_version
- name: Determine output directory
set_fact:
output_path: "{{ 'output-%0x' % ((2**32) | random) }}"
- name: Find all roles
ansible.builtin.find:
paths:
- "{{ (playbook_dir | default('.')) ~ '/roles' }}"
file_type: directory
depth: 1
register: result
- name: Create output directory
ansible.builtin.file:
path: "{{ output_path }}"
state: directory
- block:
- name: Include all roles
ansible.builtin.include_role:
name: "{{ item }}"
loop: "{{ result.files | map(attribute='path') | map('regex_replace', '.*/', '') | sort }}"
always:
- name: Remove output directory
ansible.builtin.file:
path: "{{ output_path }}"
state: absent

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