Compare commits

...

125 Commits

Author SHA1 Message Date
Felix Fontein
f7c0a85c72 Release 2.21.0. 2024-07-12 22:17:24 +02:00
Felix Fontein
8935ab8fdc Reformat and re-order changelogs/changelog.yaml. 2024-07-11 22:44:23 +02:00
Felix Fontein
1f39b0ff2a Add missing changelog for #784. 2024-07-11 22:35:47 +02:00
G Derber
b02fb8e9a0 certificate_complete_chain: add ability to identify ed25519 complete chains (#777)
* Add ability to identify ed25519 complete chains.

* Add ability to identify ed448 complete chains.

* Formatting updates

* Remove unnecessary imports.

* Cleanup whitespace

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

* Implement basic tests.

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

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

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

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

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

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

* Add changelog fragment

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

* Resolve doc builder error

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

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

* Set ssl_ctx_options version_added

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

* Initial application of suggestions from code review

Working on completing application of suggestions

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

* Finish applying suggestions from code review

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

* Documentation update

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

* Include value in fail output for wrong data type

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

* Handle invalid tls_ctx_option strings

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

* Minor documentation update

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

---------

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

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

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

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

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

* Lint and add changelog fragment.

* Add tests.

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

* Create 740-ecs_certificate-renewal-without-csr

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

---------

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

* Move csr / csr_content to new docs fragment.

* Simplify code.

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

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

* Fix documentation of acme_certificate's challenge_data return value.

* Also return cert_id from acme_certificate_renewal_info module.

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

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

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

* Add acme_certificate_renewal_info module.

* Allow to provide value for 'now'.

* Actually append msg_append.

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

* Add time helpers to ACME backend.

* Add changelog fragment.

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

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

* Improve docs.

* Fix typo and use right object.

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

* Add cert ID computation function.

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

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

* Add some more test cases.

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

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

* Strip timezone info from isoformat() output.

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

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

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

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

* Add parse_filter and to_filter plugins.

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

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

* luks_device: fix breaking unit tests

* luks_device: Duplicate key test case code cleanup

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

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

* luks_device: Add changelog fragment for PR #710

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

* Add allow_discards to perfomance tests

* Fix version for luks devices doc

* Update plugins/modules/luks_device.py

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

* add changelog fragment

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

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

* added allow_discards to the persistently stored option list

* allow_discards works with not only luks2 containers

* Update plugins/modules/luks_device.py

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

---------

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

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

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

* Add changelog fragment.

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

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

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

* luks_device: updated failing unit tests for keyslot support

* luks_device: improve detection of luks version

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-11 06:00:41 +02:00
Felix Fontein
cdfc881b32 Next expected release is 2.16.0. 2023-08-22 17:16:26 +02:00
154 changed files with 8868 additions and 1881 deletions

View File

@@ -46,7 +46,7 @@ variables:
resources:
containers:
- container: default
image: quay.io/ansible/azure-pipelines-test-container:4.0.1
image: quay.io/ansible/azure-pipelines-test-container:6.0.0
pool: Standard
@@ -65,6 +65,28 @@ stages:
test: 'devel/sanity/extra'
- name: Units
test: 'devel/units/1'
- stage: Ansible_2_17
displayName: Sanity & Units 2.17
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
targets:
- name: Sanity
test: '2.17/sanity/1'
- name: Units
test: '2.17/units/1'
- stage: Ansible_2_16
displayName: Sanity & Units 2.16
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
targets:
- name: Sanity
test: '2.16/sanity/1'
- name: Units
test: '2.16/units/1'
- stage: Ansible_2_15
displayName: Sanity & Units 2.15
dependsOn: []
@@ -76,28 +98,6 @@ stages:
test: '2.15/sanity/1'
- name: Units
test: '2.15/units/1'
- stage: Ansible_2_14
displayName: Sanity & Units 2.14
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
targets:
- name: Sanity
test: '2.14/sanity/1'
- name: Units
test: '2.14/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'
### Docker
- stage: Docker_devel
displayName: Docker devel
@@ -106,13 +106,45 @@ stages:
- template: templates/matrix.yml
parameters:
testFormat: devel/linux/{0}
targets:
- name: Fedora 40
test: fedora40
- name: Ubuntu 24.04
test: ubuntu2404
- name: Alpine 3.20
test: alpine320
groups:
- 1
- 2
- stage: Docker_2_17
displayName: Docker 2.17
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.17/linux/{0}
targets:
- name: Fedora 39
test: fedora39
- name: Ubuntu 22.04
test: ubuntu2204
- name: Alpine 3.19
test: alpine319
groups:
- 1
- 2
- stage: Docker_2_16
displayName: Docker 2.16
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.16/linux/{0}
targets:
- name: Fedora 38
test: fedora38
- name: openSUSE 15
test: opensuse15
- name: Ubuntu 22.04
test: ubuntu2204
- name: Alpine 3
test: alpine3
groups:
@@ -133,40 +165,6 @@ stages:
groups:
- 1
- 2
- stage: Docker_2_14
displayName: Docker 2.14
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.14/linux/{0}
targets:
- name: Fedora 36
test: fedora36
groups:
- 1
- 2
- stage: Docker_2_13
displayName: Docker 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.13/linux/{0}
targets:
- name: openSUSE 15 py2
test: opensuse15py2
- name: Fedora 35
test: fedora35
- name: Fedora 34
test: fedora34
- name: Ubuntu 18.04
test: ubuntu1804
- name: Alpine 3
test: alpine3
groups:
- 1
- 2
### Community Docker
- stage: Docker_community_devel
@@ -182,11 +180,7 @@ stages:
- name: Debian Bookworm
test: debian-bookworm/3.11
- name: ArchLinux
test: archlinux/3.11
- name: CentOS Stream 8 with Python 3.9
test: centos-stream8/3.9
- name: CentOS Stream 8 with Python 3.6
test: centos-stream8/3.6
test: archlinux/3.12
groups:
- 1
- 2
@@ -200,12 +194,14 @@ stages:
parameters:
testFormat: devel/{0}
targets:
- name: Alpine 3.18
test: alpine/3.18
- name: Fedora 38
test: fedora/38
- name: Alpine 3.20
test: alpine/3.20
- name: Fedora 40
test: fedora/40
- name: Ubuntu 22.04
test: ubuntu/22.04
- name: Ubuntu 24.04
test: ubuntu/24.04
groups:
- vm
- stage: Remote_devel
@@ -215,6 +211,40 @@ stages:
- template: templates/matrix.yml
parameters:
testFormat: devel/{0}
targets:
- name: macOS 14.3
test: macos/14.3
- name: RHEL 9.4
test: rhel/9.4
- name: FreeBSD 14.1
test: freebsd/14.1
groups:
- 1
- 2
- stage: Remote_2_17
displayName: Remote 2.17
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.17/{0}
targets:
- name: RHEL 9.3
test: rhel/9.3
- name: FreeBSD 13.3
test: freebsd/13.3
- name: FreeBSD 14.0
test: freebsd/14.0
groups:
- 1
- 2
- stage: Remote_2_16
displayName: Remote 2.16
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.16/{0}
targets:
- name: macOS 13.2
test: macos/13.2
@@ -222,8 +252,8 @@ stages:
test: rhel/9.2
- name: RHEL 8.8
test: rhel/8.8
- name: FreeBSD 13.2
test: freebsd/13.2
# - name: FreeBSD 13.2
# test: freebsd/13.2
groups:
- 1
- 2
@@ -241,42 +271,10 @@ stages:
test: rhel/8.7
- name: RHEL 7.9
test: rhel/7.9
- name: FreeBSD 13.1
test: freebsd/13.1
- name: FreeBSD 12.4
test: freebsd/12.4
groups:
- 1
- 2
- stage: Remote_2_14
displayName: Remote 2.14
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.14/{0}
targets:
- name: macOS 12.0
test: macos/12.0
- name: RHEL 9.0
test: rhel/9.0
#- name: FreeBSD 12.4
# test: freebsd/12.4
groups:
- 1
- 2
- stage: Remote_2_13
displayName: Remote 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.13/{0}
targets:
- name: RHEL 8.5
test: rhel/8.5
#- name: FreeBSD 13.1
# test: freebsd/13.1
# - name: FreeBSD 13.1
# test: freebsd/13.1
# - name: FreeBSD 12.4
# test: freebsd/12.4
groups:
- 1
- 2
@@ -290,13 +288,40 @@ stages:
nameFormat: Python {0}
testFormat: devel/generic/{0}
targets:
- test: 2.7
- test: 3.6
- test: 3.7
# - test: 3.8
- test: 3.8
# - test: 3.9
# - test: "3.10"
- test: "3.11"
- test: "3.13"
groups:
- 1
- 2
- stage: Generic_2_17
displayName: Generic 2.17
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Python {0}
testFormat: 2.17/generic/{0}
targets:
- test: "3.7"
- test: "3.12"
groups:
- 1
- 2
- stage: Generic_2_16
displayName: Generic 2.16
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Python {0}
testFormat: 2.16/generic/{0}
targets:
- test: "2.7"
- test: "3.6"
- test: "3.11"
groups:
- 1
- 2
@@ -314,32 +339,6 @@ stages:
groups:
- 1
- 2
- stage: Generic_2_14
displayName: Generic 2.14
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Python {0}
testFormat: 2.14/generic/{0}
targets:
- test: 3.9
groups:
- 1
- 2
- stage: Generic_2_13
displayName: Generic 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Python {0}
testFormat: 2.13/generic/{0}
targets:
- test: 3.8
groups:
- 1
- 2
## Finally
@@ -347,22 +346,22 @@ stages:
condition: succeededOrFailed()
dependsOn:
- Ansible_devel
- Ansible_2_17
- Ansible_2_16
- Ansible_2_15
- Ansible_2_14
- Ansible_2_13
- Remote_devel_extra_vms
- Remote_devel
- Remote_2_17
- Remote_2_16
- Remote_2_15
- Remote_2_14
- Remote_2_13
- Docker_devel
- Docker_2_17
- Docker_2_16
- Docker_2_15
- Docker_2_14
- Docker_2_13
- Docker_community_devel
- Generic_devel
- Generic_2_17
- Generic_2_16
- Generic_2_15
- Generic_2_14
- Generic_2_13
jobs:
- template: templates/coverage.yml

View File

@@ -33,6 +33,8 @@ jobs:
- '2.10'
- '2.11'
- '2.12'
- '2.13'
- '2.14'
# Ansible-test on various stable branches does not yet work well with cgroups v2.
# Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04
# image for these stable branches. The list of branches where this is necessary will
@@ -46,8 +48,9 @@ jobs:
- name: Perform sanity testing
uses: felixfontein/ansible-test-gh-action@main
with:
ansible-core-github-repository-slug: ${{ contains(fromJson('["2.10", "2.11"]'), matrix.ansible) && 'felixfontein/ansible' || 'ansible/ansible' }}
ansible-core-github-repository-slug: ${{ contains(fromJson('["2.9", "2.10", "2.11"]'), matrix.ansible) && 'ansible-community/eol-ansible' || 'ansible/ansible' }}
ansible-core-version: stable-${{ matrix.ansible }}
codecov-token: ${{ secrets.CODECOV_TOKEN }}
coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }}
pull-request-change-detection: 'true'
testing-type: sanity
@@ -72,6 +75,8 @@ jobs:
- '2.10'
- '2.11'
- '2.12'
- '2.13'
- '2.14'
steps:
- name: >-
@@ -79,8 +84,9 @@ jobs:
Ansible version ${{ matrix.ansible }}
uses: felixfontein/ansible-test-gh-action@main
with:
ansible-core-github-repository-slug: ${{ contains(fromJson('["2.10", "2.11"]'), matrix.ansible) && 'felixfontein/ansible' || 'ansible/ansible' }}
ansible-core-github-repository-slug: ${{ contains(fromJson('["2.9", "2.10", "2.11"]'), matrix.ansible) && 'ansible-community/eol-ansible' || 'ansible/ansible' }}
ansible-core-version: stable-${{ matrix.ansible }}
codecov-token: ${{ secrets.CODECOV_TOKEN }}
coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }}
pull-request-change-detection: 'true'
testing-type: units
@@ -111,14 +117,6 @@ jobs:
- ansible: ''
include:
# 2.9
- ansible: '2.9'
docker: fedora31
python: ''
target: azp/posix/1/
- ansible: '2.9'
docker: fedora31
python: ''
target: azp/posix/2/
- ansible: '2.9'
docker: ubuntu1804
python: ''
@@ -153,14 +151,6 @@ jobs:
python: '3.6'
target: azp/generic/2/
# 2.11
- ansible: '2.11'
docker: fedora32
python: ''
target: azp/posix/1/
- ansible: '2.11'
docker: fedora32
python: ''
target: azp/posix/2/
- ansible: '2.11'
docker: alpine3
python: ''
@@ -202,6 +192,72 @@ jobs:
docker: default
python: '3.9'
target: azp/generic/2/
# 2.13
- ansible: '2.13'
docker: opensuse15py2
python: ''
target: azp/posix/1/
- ansible: '2.13'
docker: opensuse15py2
python: ''
target: azp/posix/2/
- ansible: '2.13'
docker: fedora35
python: ''
target: azp/posix/1/
- ansible: '2.13'
docker: fedora35
python: ''
target: azp/posix/2/
- ansible: '2.13'
docker: fedora34
python: ''
target: azp/posix/1/
- ansible: '2.13'
docker: fedora34
python: ''
target: azp/posix/2/
- ansible: '2.13'
docker: ubuntu1804
python: ''
target: azp/posix/1/
- ansible: '2.13'
docker: ubuntu1804
python: ''
target: azp/posix/2/
- ansible: '2.13'
docker: alpine3
python: ''
target: azp/posix/1/
- ansible: '2.13'
docker: alpine3
python: ''
target: azp/posix/2/
- ansible: '2.13'
docker: default
python: '3.8'
target: azp/generic/1/
- ansible: '2.13'
docker: default
python: '3.8'
target: azp/generic/2/
# 2.14
- ansible: '2.14'
docker: ubuntu2004
python: ''
target: azp/posix/1/
- ansible: '2.14'
docker: ubuntu2004
python: ''
target: azp/posix/2/
- ansible: '2.14'
docker: default
python: '3.9'
target: azp/generic/1/
- ansible: '2.14'
docker: default
python: '3.9'
target: azp/generic/2/
steps:
- name: >-
@@ -210,8 +266,9 @@ jobs:
under Python ${{ matrix.python }}
uses: felixfontein/ansible-test-gh-action@main
with:
ansible-core-github-repository-slug: ${{ contains(fromJson('["2.10", "2.11"]'), matrix.ansible) && 'felixfontein/ansible' || 'ansible/ansible' }}
ansible-core-github-repository-slug: ${{ contains(fromJson('["2.9", "2.10", "2.11"]'), matrix.ansible) && 'ansible-community/eol-ansible' || 'ansible/ansible' }}
ansible-core-version: stable-${{ matrix.ansible }}
codecov-token: ${{ secrets.CODECOV_TOKEN }}
coverage: ${{ github.event_name == 'schedule' && 'always' || 'never' }}
docker-image: ${{ matrix.docker }}
integration-continue-on-error: 'false'

View File

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

View File

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

View File

@@ -78,25 +78,15 @@ jobs:
pre_base: '"#"'
# We don't have PyOpenSSL for Python 3.9
extra_vars: -e has_no_pyopenssl=true
- name: ansible-core 2.12 @ CentOS Stream 8
ansible_core: https://github.com/ansible/ansible/archive/stable-2.12.tar.gz
ansible_runner: ansible-runner
other_deps: |2
python_interpreter:
package_system: python39 python39-pip python39-wheel python39-cryptography
base_image: quay.io/centos/centos:stream8
pre_base: '"#"'
# We don't have PyOpenSSL for Python 3.9
extra_vars: -e has_no_pyopenssl=true
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.11'

20
.github/workflows/import-galaxy.yml vendored Normal file
View File

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

View File

@@ -21,14 +21,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install dependencies
run: |
pip install reuse
- name: Check REUSE compliance (except some PEM files)
- name: Remove some files before checking REUSE compliance
run: |
rm -f tests/integration/targets/*/files/*.pem
rm -f tests/integration/targets/*/files/roots/*.pem
reuse lint
- name: REUSE Compliance Check
uses: fsfe/reuse-action@v4

1463
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

3
CHANGELOG.md.license Normal file
View File

@@ -0,0 +1,3 @@
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-FileCopyrightText: Ansible Project

View File

@@ -4,6 +4,208 @@ Community Crypto Release Notes
.. contents:: Topics
v2.21.0
=======
Release Summary
---------------
Feature release.
Minor Changes
-------------
- certificate_complete_chain - add ability to identify Ed25519 and Ed448 complete chains (https://github.com/ansible-collections/community.crypto/pull/777).
- get_certificate - adds ``tls_ctx_options`` option for specifying SSL CTX options (https://github.com/ansible-collections/community.crypto/pull/779).
- get_certificate - allow to obtain the certificate chain sent by the server, and the one used for validation, with the new ``get_certificate_chain`` option. Note that this option only works if the module is run with Python 3.10 or newer (https://github.com/ansible-collections/community.crypto/issues/568, https://github.com/ansible-collections/community.crypto/pull/784).
v2.20.0
=======
Release Summary
---------------
Feature and bugfix release.
The deprecations in this release are only relevant for collections that use shared
code or docs fragments from this collection.
Minor Changes
-------------
- acme_certificate - add ``include_renewal_cert_id`` option to allow requesting renewal of a specific certificate according to the current ACME Renewal Information specification draft (https://github.com/ansible-collections/community.crypto/pull/739).
Deprecated Features
-------------------
- acme documentation fragment - the default ``community.crypto.acme[.documentation]`` docs fragment is deprecated and will be removed from community.crypto 3.0.0. Replace it with both the new ``community.crypto.acme.basic`` and ``community.crypto.acme.account`` fragments (https://github.com/ansible-collections/community.crypto/pull/735).
- acme.backends module utils - the ``get_cert_information()`` method for a ACME crypto backend must be implemented from community.crypto 3.0.0 on (https://github.com/ansible-collections/community.crypto/pull/736).
- crypto.module_backends.common module utils - the ``crypto.module_backends.common`` module utils is deprecated and will be removed from community.crypto 3.0.0. Use the improved ``argspec`` module util instead (https://github.com/ansible-collections/community.crypto/pull/749).
Bugfixes
--------
- x509_crl, x509_certificate, x509_certificate_info - when parsing absolute timestamps which omitted the second count, the first digit of the minutes was used as a one-digit minutes count, and the second digit of the minutes as a one-digit second count (https://github.com/ansible-collections/community.crypto/pull/745).
New Modules
-----------
- community.crypto.acme_ari_info - Retrieves ACME Renewal Information (ARI) for a certificate.
- community.crypto.acme_certificate_deactivate_authz - Deactivate all authz for an ACME v2 order.
- community.crypto.acme_certificate_renewal_info - Determine whether a certificate should be renewed or not.
v2.19.1
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- crypto.math module utils - change return values for ``quick_is_not_prime()`` and ``convert_int_to_bytes(0, 0)`` for special cases that do not appear when using the collection (https://github.com/ansible-collections/community.crypto/pull/733).
- ecs_certificate - fixed ``csr`` option to be empty and allow renewal of a specific certificate according to the Renewal Information specification (https://github.com/ansible-collections/community.crypto/pull/740).
- x509_certificate - since community.crypto 2.19.0 the module was no longer idempotent with respect to ``not_before`` and ``not_after`` times. This is now fixed (https://github.com/ansible-collections/community.crypto/issues/753, https://github.com/ansible-collections/community.crypto/pull/754).
v2.19.0
=======
Release Summary
---------------
Bugfix and feature release.
Minor Changes
-------------
- When using cryptography >= 42.0.0, use offset-aware ``datetime.datetime`` objects (with timezone UTC) instead of offset-naive UTC timestamps (https://github.com/ansible-collections/community.crypto/issues/726, https://github.com/ansible-collections/community.crypto/pull/727).
- openssh_cert - avoid UTC functions deprecated in Python 3.12 when using Python 3 (https://github.com/ansible-collections/community.crypto/pull/727).
Deprecated Features
-------------------
- acme.backends module utils - from community.crypto on, all implementations of ``CryptoBackend`` must override ``get_ordered_csr_identifiers()``. The current default implementation, which simply sorts the result of ``get_csr_identifiers()``, will then be removed (https://github.com/ansible-collections/community.crypto/pull/725).
Bugfixes
--------
- acme_certificate - respect the order of the CNAME and SAN identifiers that are passed on when creating an ACME order (https://github.com/ansible-collections/community.crypto/issues/723, https://github.com/ansible-collections/community.crypto/pull/725).
New Modules
-----------
- community.crypto.x509_certificate_convert - Convert X.509 certificates
v2.18.0
=======
Release Summary
---------------
Bugfix and feature release.
Minor Changes
-------------
- x509_crl - the new option ``serial_numbers`` allow to configure in which format serial numbers can be provided to ``revoked_certificates[].serial_number``. The default is as integers (``serial_numbers=integer``) for backwards compatibility; setting ``serial_numbers=hex-octets`` allows to specify colon-separated hex octet strings like ``00:11:22:FF`` (https://github.com/ansible-collections/community.crypto/issues/687, https://github.com/ansible-collections/community.crypto/pull/715).
Deprecated Features
-------------------
- openssl_csr_pipe, openssl_privatekey_pipe, x509_certificate_pipe - the current behavior of check mode is deprecated and will change in community.crypto 3.0.0. The current behavior is similar to the modules without ``_pipe``: if the object needs to be (re-)generated, only the ``changed`` status is set, but the object is not updated. From community.crypto 3.0.0 on, the modules will ignore check mode and always act as if check mode is not active. This behavior can already achieved now by adding ``check_mode: false`` to the task. If you think this breaks your use-case of this module, please `create an issue in the community.crypto repository <https://github.com/ansible-collections/community.crypto/issues/new/choose>`__ (https://github.com/ansible-collections/community.crypto/issues/712, https://github.com/ansible-collections/community.crypto/pull/714).
Bugfixes
--------
- luks_device - fixed module a bug that prevented using ``remove_keyslot`` with the value ``0`` (https://github.com/ansible-collections/community.crypto/pull/710).
- luks_device - fixed module falsely outputting ``changed=false`` when trying to add a new slot with a key that is already present in another slot. The module now rejects adding keys that are already present in another slot (https://github.com/ansible-collections/community.crypto/pull/710).
- luks_device - fixed testing of LUKS passphrases in when specifying a keyslot for cryptsetup version 2.0.3. The output of this cryptsetup version slightly differs from later versions (https://github.com/ansible-collections/community.crypto/pull/710).
New Plugins
-----------
Filter
~~~~~~
- community.crypto.parse_serial - Convert a serial number as a colon-separated list of hex numbers to an integer
- community.crypto.to_serial - Convert an integer to a colon-separated list of hex numbers
v2.17.1
=======
Release Summary
---------------
Bugfix release for compatibility with cryptography 42.0.0.
Bugfixes
--------
- openssl_dhparam - was using an internal function instead of the public API to load DH param files when using the ``cryptography`` backend. The internal function was removed in cryptography 42.0.0. The module now uses the public API, which has been available since support for DH params was added to cryptography (https://github.com/ansible-collections/community.crypto/pull/698).
- openssl_privatekey_info - ``check_consistency=true`` no longer works for RSA keys with cryptography 42.0.0+ (https://github.com/ansible-collections/community.crypto/pull/701).
- openssl_privatekey_info - ``check_consistency=true`` now reports a warning if it cannot determine consistency (https://github.com/ansible-collections/community.crypto/pull/705).
v2.17.0
=======
Release Summary
---------------
Feature release.
Minor Changes
-------------
- luks_device - add allow discards option (https://github.com/ansible-collections/community.crypto/pull/693).
v2.16.2
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- acme_* modules - directly react on bad return data for account creation/retrieval/updating requests (https://github.com/ansible-collections/community.crypto/pull/682).
- acme_* modules - fix improved error reporting in case of socket errors, bad status lines, and unknown connection errors (https://github.com/ansible-collections/community.crypto/pull/684).
- acme_* modules - increase number of retries from 5 to 10 to increase stability with unstable ACME endpoints (https://github.com/ansible-collections/community.crypto/pull/685).
- acme_* modules - make account registration handling more flexible to accept 404 instead of 400 send by DigiCert's ACME endpoint when an account does not exist (https://github.com/ansible-collections/community.crypto/pull/681).
v2.16.1
=======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- acme_* modules - also retry requests in case of socket errors, bad status lines, and unknown connection errors; improve error messages in these cases (https://github.com/ansible-collections/community.crypto/issues/680).
v2.16.0
=======
Release Summary
---------------
Bugfix release.
Minor Changes
-------------
- luks_devices - add new options ``keyslot``, ``new_keyslot``, and ``remove_keyslot`` to allow adding/removing keys to/from specific keyslots (https://github.com/ansible-collections/community.crypto/pull/664).
Bugfixes
--------
- openssl_pkcs12 - modify autodetect to not detect pyOpenSSL >= 23.3.0, which removed PKCS#12 support (https://github.com/ansible-collections/community.crypto/pull/666).
v2.15.1
=======
@@ -48,12 +250,12 @@ New Plugins
Filter
~~~~~~
- gpg_fingerprint - Retrieve a GPG fingerprint from a GPG public or private key
- community.crypto.gpg_fingerprint - Retrieve a GPG fingerprint from a GPG public or private key
Lookup
~~~~~~
- gpg_fingerprint - Retrieve a GPG fingerprint from a GPG public or private key file
- community.crypto.gpg_fingerprint - Retrieve a GPG fingerprint from a GPG public or private key file
v2.14.1
=======
@@ -72,7 +274,6 @@ ansible-core 2.15 or later to see it as it is intended. Alternatively you can
look at `the devel docsite <https://docs.ansible.com/ansible/devel/collections/community/crypto/>`__
for the rendered HTML version of the documentation of the latest release.
Bugfixes
--------
@@ -197,12 +398,12 @@ New Plugins
Filter
~~~~~~
- openssl_csr_info - Retrieve information from OpenSSL Certificate Signing Requests (CSR)
- openssl_privatekey_info - Retrieve information from OpenSSL private keys
- openssl_publickey_info - Retrieve information from OpenSSL public keys in PEM format
- split_pem - Split PEM file contents into multiple objects
- x509_certificate_info - Retrieve information from X.509 certificates in PEM format
- x509_crl_info - Retrieve information from X.509 CRLs in PEM format
- community.crypto.openssl_csr_info - Retrieve information from OpenSSL Certificate Signing Requests (CSR)
- community.crypto.openssl_privatekey_info - Retrieve information from OpenSSL private keys
- community.crypto.openssl_publickey_info - Retrieve information from OpenSSL public keys in PEM format
- community.crypto.split_pem - Split PEM file contents into multiple objects
- community.crypto.x509_certificate_info - Retrieve information from X.509 certificates in PEM format
- community.crypto.x509_crl_info - Retrieve information from X.509 CRLs in PEM format
v2.9.0
======
@@ -332,7 +533,6 @@ This release is identical to what should have been 2.3.3, except that the
version number has been bumped to 2.3.4 and this changelog entry for 2.3.4
has been added.
v2.3.3
======
@@ -387,7 +587,7 @@ Minor Changes
-------------
- Prepare collection for inclusion in an Execution Environment by declaring its dependencies. Please note that system packages are used for cryptography and PyOpenSSL, which can be rather limited. If you need features from newer cryptography versions, you will have to manually force a newer version to be installed by pip by specifying something like ``cryptography >= 37.0.0`` in your Execution Environment's Python dependencies file (https://github.com/ansible-collections/community.crypto/pull/440).
- Support automatic conversion for Internalionalized Domain Names (IDNs). When passing general names, for example Subject Altenative Names to ``community.crypto.openssl_csr``, these will automatically be converted to IDNA. Conversion will be done per label to IDNA2008 if possible, and IDNA2003 if IDNA2008 conversion fails for that label. Note that IDNA conversion requires `the Python idna library <https://pypi.org/project/idna/>`_ to be installed. Please note that depending on which versions of the cryptography library are used, it could try to process the converted IDNA another time with the Python ``idna`` library and reject IDNA2003 encoded values. Using a new enough ``cryptography`` version avoids this (https://github.com/ansible-collections/community.crypto/issues/426, https://github.com/ansible-collections/community.crypto/pull/436).
- Support automatic conversion for Internalionalized Domain Names (IDNs). When passing general names, for example Subject Alternative Names to ``community.crypto.openssl_csr``, these will automatically be converted to IDNA. Conversion will be done per label to IDNA2008 if possible, and IDNA2003 if IDNA2008 conversion fails for that label. Note that IDNA conversion requires `the Python idna library <https://pypi.org/project/idna/>`_ to be installed. Please note that depending on which versions of the cryptography library are used, it could try to process the converted IDNA another time with the Python ``idna`` library and reject IDNA2003 encoded values. Using a new enough ``cryptography`` version avoids this (https://github.com/ansible-collections/community.crypto/issues/426, https://github.com/ansible-collections/community.crypto/pull/436).
- acme_* modules - add parameter ``request_timeout`` to manage HTTP(S) request timeout (https://github.com/ansible-collections/community.crypto/issues/447, https://github.com/ansible-collections/community.crypto/pull/448).
- 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).
@@ -439,7 +639,6 @@ Regular bugfix release.
In this release, we extended the test matrix to include Alpine 3, ArchLinux, Debian Bullseye, and CentOS Stream 8. CentOS 8 was removed from the test matrix.
Bugfixes
--------
@@ -503,8 +702,8 @@ Bugfixes
New Modules
-----------
- crypto_info - Retrieve cryptographic capabilities
- openssl_privatekey_convert - Convert OpenSSL private keys
- community.crypto.crypto_info - Retrieve cryptographic capabilities
- community.crypto.openssl_privatekey_convert - Convert OpenSSL private keys
v2.0.2
======
@@ -543,7 +742,6 @@ Release Summary
A new major release of the ``community.crypto`` collection. The main changes are removal of the PyOpenSSL backends for almost all modules (``openssl_pkcs12`` being the only exception), and removal of the ``assertonly`` provider in the ``x509_certificate`` provider. There are also some other breaking changes which should improve the user interface/experience of this collection long-term.
Minor Changes
-------------
@@ -726,20 +924,20 @@ Minor Changes
- openssh_keypair - added ``passphrase`` parameter for encrypting/decrypting OpenSSH private keys (https://github.com/ansible-collections/community.crypto/pull/225).
- openssl_csr - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- openssl_csr_info - now returns ``public_key_type`` and ``public_key_data`` (https://github.com/ansible-collections/community.crypto/pull/233).
- openssl_csr_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/204).
- openssl_csr_info - refactor module to allow code reuse for diff mode (https://github.com/ansible-collections/community.crypto/pull/204).
- openssl_csr_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- openssl_pkcs12 - added option ``select_crypto_backend`` and a ``cryptography`` backend. This requires cryptography 3.0 or newer, and does not support the ``iter_size`` and ``maciter_size`` options (https://github.com/ansible-collections/community.crypto/pull/234).
- openssl_privatekey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- openssl_privatekey_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/205).
- openssl_privatekey_info - refactor module to allow code reuse for diff mode (https://github.com/ansible-collections/community.crypto/pull/205).
- openssl_privatekey_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- openssl_publickey - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- x509_certificate - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- x509_certificate_info - now returns ``public_key_type`` and ``public_key_data`` (https://github.com/ansible-collections/community.crypto/pull/233).
- x509_certificate_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/206).
- x509_certificate_info - refactor module to allow code reuse for diff mode (https://github.com/ansible-collections/community.crypto/pull/206).
- x509_certificate_pipe - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- x509_crl - add diff mode (https://github.com/ansible-collections/community.crypto/issues/38, https://github.com/ansible-collections/community.crypto/pull/150).
- x509_crl_info - add ``list_revoked_certificates`` option to avoid enumerating all revoked certificates (https://github.com/ansible-collections/community.crypto/pull/232).
- x509_crl_info - refactor module to allow code re-use for diff mode (https://github.com/ansible-collections/community.crypto/pull/203).
- x509_crl_info - refactor module to allow code reuse for diff mode (https://github.com/ansible-collections/community.crypto/pull/203).
Bugfixes
--------
@@ -751,7 +949,7 @@ Bugfixes
New Modules
-----------
- openssl_publickey_info - Provide information for OpenSSL public keys
- community.crypto.openssl_publickey_info - Provide information for OpenSSL public keys
v1.6.2
======
@@ -862,16 +1060,15 @@ Release Summary
Contains new modules ``openssl_privatekey_pipe``, ``openssl_csr_pipe`` and ``x509_certificate_pipe`` which allow to create or update private keys, CSRs and X.509 certificates without having to write them to disk.
Minor Changes
-------------
- openssh_cert - add module parameter ``use_agent`` to enable using signing keys stored in ssh-agent (https://github.com/ansible-collections/community.crypto/issues/116).
- openssl_csr - refactor module to allow code re-use by openssl_csr_pipe (https://github.com/ansible-collections/community.crypto/pull/123).
- openssl_privatekey - refactor module to allow code re-use by openssl_privatekey_pipe (https://github.com/ansible-collections/community.crypto/pull/119).
- openssl_csr - refactor module to allow code reuse by openssl_csr_pipe (https://github.com/ansible-collections/community.crypto/pull/123).
- openssl_privatekey - refactor module to allow code reuse by openssl_privatekey_pipe (https://github.com/ansible-collections/community.crypto/pull/119).
- openssl_privatekey - the elliptic curve ``secp192r1`` now triggers a security warning. Elliptic curves of at least 224 bits should be used for new keys; see `here <https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec.html#elliptic-curves>`_ (https://github.com/ansible-collections/community.crypto/pull/132).
- x509_certificate - for the ``selfsigned`` provider, a CSR is not required anymore. If no CSR is provided, the module behaves as if a minimal CSR which only contains the public key has been provided (https://github.com/ansible-collections/community.crypto/issues/32, https://github.com/ansible-collections/community.crypto/pull/129).
- x509_certificate - refactor module to allow code re-use by x509_certificate_pipe (https://github.com/ansible-collections/community.crypto/pull/135).
- x509_certificate - refactor module to allow code reuse by x509_certificate_pipe (https://github.com/ansible-collections/community.crypto/pull/135).
Bugfixes
--------
@@ -883,9 +1080,9 @@ Bugfixes
New Modules
-----------
- openssl_csr_pipe - Generate OpenSSL Certificate Signing Request (CSR)
- openssl_privatekey_pipe - Generate OpenSSL private keys without disk access
- x509_certificate_pipe - Generate and/or check OpenSSL certificates
- community.crypto.openssl_csr_pipe - Generate OpenSSL Certificate Signing Request (CSR)
- community.crypto.openssl_privatekey_pipe - Generate OpenSSL private keys without disk access
- community.crypto.x509_certificate_pipe - Generate and/or check OpenSSL certificates
v1.2.0
======
@@ -938,7 +1135,6 @@ Release Summary
Release for Ansible 2.10.0.
Minor Changes
-------------
@@ -962,8 +1158,8 @@ Bugfixes
New Modules
-----------
- openssl_signature - Sign data with openssl
- openssl_signature_info - Verify signatures with openssl
- community.crypto.openssl_signature - Sign data with openssl
- community.crypto.openssl_signature_info - Verify signatures with openssl
v1.0.0
======
@@ -973,7 +1169,6 @@ Release Summary
This is the first proper release of the ``community.crypto`` collection. This changelog contains all changes to the modules in this collection that were added after the release of Ansible 2.9.0.
Minor Changes
-------------
@@ -984,7 +1179,7 @@ Minor Changes
- openssh_keypair - instead of regenerating some broken or password protected keys, fail the module. Keys can still be regenerated by calling the module with ``force=yes``.
- openssh_keypair - the ``regenerate`` option allows to configure the module's behavior when it should or needs to regenerate private keys.
- openssl_* modules - the cryptography backend now properly supports ``dirName``, ``otherName`` and ``RID`` (Registered ID) names.
- openssl_certificate - Add option for changing which ACME directory to use with acme-tiny. Set the default ACME directory to Let's Encrypt instead of using acme-tiny's default. (acme-tiny also uses Let's Encrypt at the time being, so no action should be neccessary.)
- openssl_certificate - Add option for changing which ACME directory to use with acme-tiny. Set the default ACME directory to Let's Encrypt instead of using acme-tiny's default. (acme-tiny also uses Let's Encrypt at the time being, so no action should be necessary.)
- openssl_certificate - Change the required version of acme-tiny to >= 4.0.0
- openssl_certificate - allow to provide content of some input files via the ``csr_content``, ``privatekey_content``, ``ownca_privatekey_content`` and ``ownca_content`` options.
- openssl_certificate - allow to return the existing/generated certificate directly as ``certificate`` by setting ``return_content`` to ``yes``.
@@ -1039,6 +1234,6 @@ Bugfixes
New Modules
-----------
- ecs_domain - Request validation of a domain with the Entrust Certificate Services (ECS) API
- x509_crl - Generate Certificate Revocation Lists (CRLs)
- x509_crl_info - Retrieve information on Certificate Revocation Lists (CRLs)
- community.crypto.ecs_domain - Request validation of a domain with the Entrust Certificate Services (ECS) API
- community.crypto.x509_crl - Generate Certificate Revocation Lists (CRLs)
- community.crypto.x509_crl_info - Retrieve information on Certificate Revocation Lists (CRLs)

View File

@@ -18,7 +18,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, ansible-core 2.12, ansible-core 2.13, ansible-core 2.14, and ansible-core-2.15 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12, ansible-core 2.13, ansible-core 2.14, ansible-core 2.15, ansible-core 2.16, and ansible-core-2.17 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
## External requirements
@@ -54,6 +54,7 @@ If you use the Ansible package and do not update collections independently, use
- openssl_signature_info module
- openssl_signature module
- split_pem filter
- x509_certificate_convert module
- x509_certificate_info module and filter
- x509_certificate_pipe module
- x509_certificate module
@@ -65,7 +66,9 @@ If you use the Ansible package and do not update collections independently, use
- ACME modules and plugins:
- acme_account_info module
- acme_account module
- acme_ari_info module
- acme_certificate module
- acme_certificate_deactivate_authz module
- acme_certificate_revoke module
- acme_challenge_cert_helper module
- acme_inspect module
@@ -78,6 +81,7 @@ If you use the Ansible package and do not update collections independently, use
- crypto_info module
- get_certificate module
- luks_device module
- parse_serial and to_serial filters
You can also find a list of all modules and plugins with documentation on the [Ansible docs site](https://docs.ansible.com/ansible/latest/collections/community/crypto/), or the [latest commit collection documentation](https://ansible-collections.github.io/community.crypto/branch/main/).
@@ -111,7 +115,7 @@ See [Ansible's dev guide](https://docs.ansible.com/ansible/devel/dev_guide/devel
## Release notes
See the [changelog](https://github.com/ansible-collections/community.crypto/blob/main/CHANGELOG.rst).
See the [changelog](https://github.com/ansible-collections/community.crypto/blob/main/CHANGELOG.md).
## Roadmap

File diff suppressed because it is too large Load Diff

View File

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

7
docs/docsite/config.yml Normal file
View File

@@ -0,0 +1,7 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
changelog:
write_changelog: true

View File

@@ -25,3 +25,7 @@ communication:
mailing_lists:
- topic: Ansible Project List
url: https://groups.google.com/g/ansible-project
forums:
- topic: Ansible Forum
# The following URL directly points to the "Get Help" section
url: https://forum.ansible.com/c/help/6/none

View File

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

View File

@@ -8,7 +8,7 @@
How to create self-signed certificates
======================================
The `community.crypto collection <https://galaxy.ansible.com/community/crypto>`_ offers multiple modules that create private keys, certificate signing requests, and certificates. This guide shows how to create self-signed certificates.
The `community.crypto collection <https://galaxy.ansible.com/ui/repo/published/community/crypto/>`_ offers multiple modules that create private keys, certificate signing requests, and certificates. This guide shows how to create self-signed certificates.
For creating any kind of certificate, you always have to start with a private key. You can use the :ref:`community.crypto.openssl_privatekey module <ansible_collections.community.crypto.openssl_privatekey_module>` to create a private key. If you only specify :ansopt:`community.crypto.openssl_privatekey#module:path`, the default parameters will be used. This will result in a 4096 bit RSA private key:

View File

@@ -5,7 +5,7 @@
namespace: community
name: crypto
version: 2.15.1
version: 2.21.0
readme: README.md
authors:
- Ansible (github.com/ansible)

View File

@@ -8,6 +8,7 @@ requires_ansible: '>=2.9.10'
action_groups:
acme:
- acme_inspect
- acme_certificate_deactivate_authz
- acme_certificate_revoke
- acme_certificate
- acme_account

View File

@@ -51,6 +51,16 @@ class PrivateKeyModule(object):
self.module_backend.generate_private_key()
privatekey_data = self.module_backend.get_private_key_data()
self.privatekey_bytes = privatekey_data
else:
self.module.deprecate(
'Check mode support for openssl_privatekey_pipe will change in community.crypto 3.0.0'
' to behave the same as without check mode. You can get that behavior right now'
' by adding `check_mode: false` to the openssl_privatekey_pipe task. If you think this'
' breaks your use-case of this module, please create an issue in the'
' community.crypto repository',
version='3.0.0',
collection_name='community.crypto',
)
self.changed = True
elif self.module_backend.needs_conversion():
# Convert
@@ -58,6 +68,16 @@ class PrivateKeyModule(object):
self.module_backend.convert_private_key()
privatekey_data = self.module_backend.get_private_key_data()
self.privatekey_bytes = privatekey_data
else:
self.module.deprecate(
'Check mode support for openssl_privatekey_pipe will change in community.crypto 3.0.0'
' to behave the same as without check mode. You can get that behavior right now'
' by adding `check_mode: false` to the openssl_privatekey_pipe task. If you think this'
' breaks your use-case of this module, please create an issue in the'
' community.crypto repository',
version='3.0.0',
collection_name='community.crypto',
)
self.changed = True
def dump(self):

View File

@@ -11,6 +11,9 @@ __metaclass__ = type
class ModuleDocFragment(object):
# Standard files documentation fragment
#
# NOTE: This document fragment is DEPRECATED and will be removed from community.crypto 3.0.0.
# Use both the BASIC and ACCOUNT fragments as a replacement.
DOCUMENTATION = r'''
notes:
- "If a new enough version of the C(cryptography) library
@@ -137,3 +140,178 @@ options:
default: 10
version_added: 2.3.0
'''
# Basic documentation fragment without account data
BASIC = r'''
notes:
- "Although the defaults are chosen so that the module can be used with
the L(Let's Encrypt,https://letsencrypt.org/) CA, the module can in
principle be used with any CA providing an ACME endpoint, such as
L(Buypass Go SSL,https://www.buypass.com/ssl/products/acme)."
- "So far, the ACME modules have only been tested by the developers against
Let's Encrypt (staging and production), Buypass (staging and production), ZeroSSL (production),
and L(Pebble testing server,https://github.com/letsencrypt/Pebble). We have got
community feedback that they also work with Sectigo ACME Service for InCommon.
If you experience problems with another ACME server, please
L(create an issue,https://github.com/ansible-collections/community.crypto/issues/new/choose)
to help us supporting it. Feedback that an ACME server not mentioned does work
is also appreciated."
requirements:
- either openssl or L(cryptography,https://cryptography.io/) >= 1.5
- ipaddress
options:
acme_version:
description:
- "The ACME version of the endpoint."
- "Must be V(1) for the classic Let's Encrypt and Buypass ACME endpoints,
or V(2) for standardized ACME v2 endpoints."
- "The value V(1) is deprecated since community.crypto 2.0.0 and will be
removed from community.crypto 3.0.0."
required: true
type: int
choices: [ 1, 2 ]
acme_directory:
description:
- "The ACME directory to use. This is the entry point URL to access
the ACME CA server API."
- "For safety reasons the default is set to the Let's Encrypt staging
server (for the ACME v1 protocol). This will create technically correct,
but untrusted certificates."
- "For Let's Encrypt, all staging endpoints can be found here:
U(https://letsencrypt.org/docs/staging-environment/). For Buypass, all
endpoints can be found here:
U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)"
- "For B(Let's Encrypt), the production directory URL for ACME v2 is
U(https://acme-v02.api.letsencrypt.org/directory)."
- "For B(Buypass), the production directory URL for ACME v2 and v1 is
U(https://api.buypass.com/acme/directory)."
- "For B(ZeroSSL), the production directory URL for ACME v2 is
U(https://acme.zerossl.com/v2/DV90)."
- "For B(Sectigo), the production directory URL for ACME v2 is
U(https://acme-qa.secure.trust-provider.com/v2/DV)."
- The notes for this module contain a list of ACME services this module has
been tested against.
required: true
type: str
validate_certs:
description:
- Whether calls to the ACME directory will validate TLS certificates.
- "B(Warning:) Should B(only ever) be set to V(false) for testing purposes,
for example when testing against a local Pebble server."
type: bool
default: true
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is V(auto), which tries to use C(cryptography) if available, and falls back to
C(openssl).
- If set to V(openssl), will try to use the C(openssl) binary.
- If set to V(cryptography), will try to use the
L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [ auto, cryptography, openssl ]
request_timeout:
description:
- The time Ansible should wait for a response from the ACME API.
- This timeout is applied to all HTTP(S) requests (HEAD, GET, POST).
type: int
default: 10
version_added: 2.3.0
'''
# Account data documentation fragment
ACCOUNT = r'''
notes:
- "If a new enough version of the C(cryptography) library
is available (see Requirements for details), it will be used
instead of the C(openssl) binary. This can be explicitly disabled
or enabled with the O(select_crypto_backend) option. Note that using
the C(openssl) binary will be slower and less secure, as private key
contents always have to be stored on disk (see
O(account_key_content))."
options:
account_key_src:
description:
- "Path to a file containing the ACME account RSA or Elliptic Curve
key."
- "Private keys can be created with the
M(community.crypto.openssl_privatekey) or M(community.crypto.openssl_privatekey_pipe)
modules. If the requisite (cryptography) is not available,
keys can also be created directly with the C(openssl) command line tool:
RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys
can be created with C(openssl ecparam -genkey ...). Any other tool creating
private keys in PEM format can be used as well."
- "Mutually exclusive with O(account_key_content)."
- "Required if O(account_key_content) is not used."
type: path
aliases: [ account_key ]
account_key_content:
description:
- "Content of the ACME account RSA or Elliptic Curve key."
- "Mutually exclusive with O(account_key_src)."
- "Required if O(account_key_src) is not used."
- "B(Warning:) the content will be written into a temporary file, which will
be deleted by Ansible when the module completes. Since this is an
important private key — it can be used to change the account key,
or to revoke your certificates without knowing their private keys
—, this might not be acceptable."
- "In case C(cryptography) is used, the content is not written into a
temporary file. It can still happen that it is written to disk by
Ansible in the process of moving the module with its argument to
the node where it is executed."
type: str
account_key_passphrase:
description:
- Phassphrase to use to decode the account key.
- "B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend."
type: str
version_added: 1.6.0
account_uri:
description:
- "If specified, assumes that the account URI is as given. If the
account key does not match this account, or an account with this
URI does not exist, the module fails."
type: str
'''
# No account data documentation fragment
NO_ACCOUNT = r'''
notes:
- "If a new enough version of the C(cryptography) library
is available (see Requirements for details), it will be used
instead of the C(openssl) binary. This can be explicitly disabled
or enabled with the O(select_crypto_backend) option. Note that using
the C(openssl) binary will be slower."
options: {}
'''
CERTIFICATE = r'''
options:
csr:
description:
- "File containing the CSR for the new certificate."
- "Can be created with M(community.crypto.openssl_csr)."
- "The CSR may contain multiple Subject Alternate Names, but each one
will lead to an individual challenge that must be fulfilled for the
CSR to be signed."
- "B(Note): the private key used to create the CSR B(must not) be the
account key. This is a bad idea from a security point of view, and
the CA should not accept the CSR. The ACME server should return an
error in this case."
- Precisely one of O(csr) or O(csr_content) must be specified.
type: path
csr_content:
description:
- "Content of the CSR for the new certificate."
- "Can be created with M(community.crypto.openssl_csr_pipe)."
- "The CSR may contain multiple Subject Alternate Names, but each one
will lead to an individual challenge that must be fulfilled for the
CSR to be signed."
- "B(Note): the private key used to create the CSR B(must not) be the
account key. This is a bad idea from a security point of view, and
the CA should not accept the CSR. The ACME server should return an
error in this case."
- Precisely one of O(csr) or O(csr_content) must be specified.
type: str
'''

View File

@@ -266,6 +266,8 @@ options:
or for own CAs."
- The C(AuthorityKeyIdentifier) extension will only be added if at least one of O(authority_key_identifier),
O(authority_cert_issuer) and O(authority_cert_serial_number) is specified.
- This option accepts an B(integer). If you want to provide serial numbers as colon-separated hex strings,
such as C(11:22:33), you need to convert them to an integer with P(community.crypto.parse_serial#filter).
type: int
crl_distribution_points:
description:
@@ -322,4 +324,6 @@ seealso:
- module: community.crypto.openssl_privatekey_pipe
- module: community.crypto.openssl_publickey
- module: community.crypto.openssl_csr_info
- plugin: community.crypto.parse_serial
plugin_type: filter
'''

View File

@@ -26,6 +26,8 @@ extends_documentation_fragment:
- community.crypto.name_encoding
seealso:
- module: community.crypto.openssl_csr_info
- plugin: community.crypto.to_serial
plugin_type: filter
'''
EXAMPLES = '''
@@ -268,6 +270,8 @@ _value:
description:
- The CSR's authority cert serial number.
- Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
returned: success
type: int
sample: 12345

View File

@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = """
name: parse_serial
short_description: Convert a serial number as a colon-separated list of hex numbers to an integer
author: Felix Fontein (@felixfontein)
version_added: 2.18.0
description:
- "Parses a colon-separated list of hex numbers of the form C(00:11:22:33) and returns the corresponding integer."
options:
_input:
description:
- A serial number represented as a colon-separated list of hex numbers between 0 and 255.
- These numbers are interpreted as the byte presentation of an unsigned integer in network byte order.
That is, C(01:00) is interpreted as the integer 256.
type: string
required: true
seealso:
- plugin: community.crypto.to_serial
plugin_type: filter
"""
EXAMPLES = """
- name: Parse serial number
ansible.builtin.debug:
msg: "{{ '11:22:33' | community.crypto.parse_serial }}"
"""
RETURN = """
_value:
description:
- The serial number as an integer.
type: int
"""
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.serial import parse_serial
def parse_serial_filter(input):
if not isinstance(input, string_types):
raise AnsibleFilterError(
'The input for the community.crypto.parse_serial filter must be a string; got {type} instead'.format(type=type(input))
)
try:
return parse_serial(to_native(input))
except ValueError as exc:
raise AnsibleFilterError(to_native(exc))
class FilterModule(object):
'''Ansible jinja2 filters'''
def filters(self):
return {
'parse_serial': parse_serial_filter,
}

View File

@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = """
name: to_serial
short_description: Convert an integer to a colon-separated list of hex numbers
author: Felix Fontein (@felixfontein)
version_added: 2.18.0
description:
- "Converts an integer to a colon-separated list of hex numbers of the form C(00:11:22:33)."
options:
_input:
description:
- The non-negative integer to convert.
type: int
required: true
seealso:
- plugin: community.crypto.to_serial
plugin_type: filter
"""
EXAMPLES = """
- name: Convert integer to serial number
ansible.builtin.debug:
msg: "{{ 1234567 | community.crypto.to_serial }}"
"""
RETURN = """
_value:
description:
- A colon-separated list of hexadecimal numbers.
- Letters are upper-case, and all numbers have exactly two digits.
- The string is never empty. The representation of C(0) is C("00").
type: string
"""
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import integer_types
from ansible_collections.community.crypto.plugins.module_utils.serial import to_serial
def to_serial_filter(input):
if not isinstance(input, integer_types):
raise AnsibleFilterError(
'The input for the community.crypto.to_serial filter must be an integer; got {type} instead'.format(type=type(input))
)
if input < 0:
raise AnsibleFilterError('The input for the community.crypto.to_serial filter must not be negative')
try:
return to_serial(input)
except ValueError as exc:
raise AnsibleFilterError(to_native(exc))
class FilterModule(object):
'''Ansible jinja2 filters'''
def filters(self):
return {
'to_serial': to_serial_filter,
}

View File

@@ -26,6 +26,8 @@ extends_documentation_fragment:
- community.crypto.name_encoding
seealso:
- module: community.crypto.x509_certificate_info
- plugin: community.crypto.to_serial
plugin_type: filter
'''
EXAMPLES = '''
@@ -253,7 +255,10 @@ _value:
type: str
sample: sha256WithRSAEncryption
serial_number:
description: The certificate's serial number.
description:
- The certificate's serial number.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
returned: success
type: int
sample: 1234
@@ -291,6 +296,8 @@ _value:
description:
- The certificate's authority cert serial number.
- Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
returned: success
type: int
sample: 12345

View File

@@ -35,6 +35,8 @@ extends_documentation_fragment:
- community.crypto.name_encoding
seealso:
- module: community.crypto.x509_crl_info
- plugin: community.crypto.to_serial
plugin_type: filter
'''
EXAMPLES = '''
@@ -100,7 +102,10 @@ _value:
elements: dict
contains:
serial_number:
description: Serial number of the certificate.
description:
- Serial number of the certificate.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
type: int
sample: 1234
revocation_date:

View File

@@ -9,6 +9,8 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.common._collections_compat import Mapping
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ACMEProtocolException,
ModuleFailException,
@@ -96,6 +98,9 @@ class ACMEAccount(object):
)
result, info = self.client.send_signed_request(url, new_reg, fail_on_error=False)
if not isinstance(result, Mapping):
raise ACMEProtocolException(
self.client.module, msg='Invalid account creation reply from ACME server', info=info, content=result)
if info['status'] in ([200, 201] if self.client.version == 1 else [201]):
# Account did not exist
@@ -118,8 +123,10 @@ class ACMEAccount(object):
if 'location' in info:
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:
elif info['status'] in (400, 404) and result['type'] == 'urn:ietf:params:acme:error:accountDoesNotExist' and not allow_creation:
# Account does not exist (and we did not try to create it)
# (According to RFC 8555, Section 7.3.1, the HTTP status code MUST be 400.
# Unfortunately Digicert does not care and sends 404 instead.)
return False, None
elif info['status'] == 403 and result['type'] == 'urn:ietf:params:acme:error:unauthorized' and 'deactivated' in (result.get('detail') or ''):
# Account has been deactivated; currently works for Pebble; has not been
@@ -154,6 +161,9 @@ class ACMEAccount(object):
# retry as a regular POST (with no changed data) for pre-draft-15 ACME servers
data = {}
result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False)
if not isinstance(result, Mapping):
raise ACMEProtocolException(
self.client.module, msg='Invalid account data retrieved from ACME server', info=info, content=result)
if info['status'] in (400, 403) and result.get('type') == 'urn:ietf:params:acme:error:unauthorized':
# Returned when account is deactivated
return None
@@ -248,5 +258,9 @@ class ACMEAccount(object):
else:
if self.client.version == 1:
update_request['resource'] = 'reg'
account_data, dummy = self.client.send_signed_request(self.client.account_uri, update_request)
account_data, info = self.client.send_signed_request(self.client.account_uri, update_request)
if not isinstance(account_data, Mapping):
raise ACMEProtocolException(
self.client.module, msg='Invalid account updating reply from ACME server', info=info, content=account_data)
return True, account_data

View File

@@ -21,6 +21,8 @@ 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.argspec import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
OpenSSLCLIBackend,
)
@@ -42,7 +44,9 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
)
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
compute_cert_id,
nopad_b64,
parse_retry_after,
)
try:
@@ -55,15 +59,19 @@ else:
IPADDRESS_IMPORT_ERROR = None
RETRY_STATUS_CODES = (408, 429, 503)
# -1 usually means connection problems
RETRY_STATUS_CODES = (-1, 408, 429, 503)
RETRY_COUNT = 10
def _decode_retry(module, response, info, retry_count):
if info['status'] not in RETRY_STATUS_CODES:
return False
if retry_count >= 5:
raise ACMEProtocolException(module, msg='Giving up after 5 retries', info=info, response=response)
if retry_count >= RETRY_COUNT:
raise ACMEProtocolException(
module, msg='Giving up after {retry} retries'.format(retry=RETRY_COUNT), info=info, response=response)
# 429 and 503 should have a Retry-After header (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
try:
@@ -149,6 +157,9 @@ class ACMEDirectory(object):
self.module, msg='Was not able to obtain nonce, giving up after 5 retries', info=info, response=response)
retry_count += 1
def has_renewal_info_endpoint(self):
return 'renewalInfo' in self.directory
class ACMEClient(object):
'''
@@ -164,9 +175,9 @@ class ACMEClient(object):
self.backend = backend
self.version = module.params['acme_version']
# account_key path and content are mutually exclusive
self.account_key_file = module.params['account_key_src']
self.account_key_content = module.params['account_key_content']
self.account_key_passphrase = module.params['account_key_passphrase']
self.account_key_file = module.params.get('account_key_src')
self.account_key_content = module.params.get('account_key_content')
self.account_key_passphrase = module.params.get('account_key_passphrase')
# Grab account URI from module parameters.
# Make sure empty string is treated as None.
@@ -379,24 +390,94 @@ class ACMEClient(object):
self.module, msg=error_msg, info=info, content=content, content_json=result if parsed_json_result else None)
return result, info
def get_renewal_info(
self,
cert_id=None,
cert_info=None,
cert_filename=None,
cert_content=None,
include_retry_after=False,
retry_after_relative_with_timezone=True,
):
if not self.directory.has_renewal_info_endpoint():
raise ModuleFailException('The ACME endpoint does not support ACME Renewal Information retrieval')
if cert_id is None:
cert_id = compute_cert_id(self.backend, cert_info=cert_info, cert_filename=cert_filename, cert_content=cert_content)
url = '{base}{cert_id}'.format(base=self.directory.directory['renewalInfo'], cert_id=cert_id)
data, info = self.get_request(url, parse_json_result=True, fail_on_error=True, get_only=True)
# Include Retry-After header if asked for
if include_retry_after and 'retry-after' in info:
try:
data['retryAfter'] = parse_retry_after(
info['retry-after'],
relative_with_timezone=retry_after_relative_with_timezone,
)
except ValueError:
pass
return data
def get_default_argspec():
'''
Provides default argument spec for the options documented in the acme doc fragment.
DEPRECATED: will be removed in community.crypto 3.0.0
'''
return dict(
account_key_src=dict(type='path', aliases=['account_key']),
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', 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),
account_key_src=dict(type='path', aliases=['account_key']),
account_key_content=dict(type='str', no_log=True),
account_key_passphrase=dict(type='str', no_log=True),
account_uri=dict(type='str'),
)
def create_default_argspec(
with_account=True,
require_account_key=True,
with_certificate=False,
):
'''
Provides default argument spec for the options documented in the acme doc fragment.
'''
result = ArgumentSpec(
argument_spec=dict(
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),
),
)
if with_account:
result.update_argspec(
account_key_src=dict(type='path', aliases=['account_key']),
account_key_content=dict(type='str', no_log=True),
account_key_passphrase=dict(type='str', no_log=True),
account_uri=dict(type='str'),
)
if require_account_key:
result.update(required_one_of=[['account_key_src', 'account_key_content']])
result.update(mutually_exclusive=[['account_key_src', 'account_key_content']])
if with_certificate:
result.update_argspec(
csr=dict(type='path'),
csr_content=dict(type='str'),
)
result.update(
required_one_of=[['csr', 'csr_content']],
mutually_exclusive=[['csr', 'csr_content']],
)
return result
def create_backend(module, needs_acme_v2):
if not HAS_IPADDRESS:
module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMPORT_ERROR)

View File

@@ -13,7 +13,6 @@ import base64
import binascii
import datetime
import os
import sys
import traceback
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
@@ -21,7 +20,9 @@ from ansible.module_utils.common.text.converters import to_bytes, to_native, to_
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
CertificateInformation,
CryptoBackend,
_parse_acme_timestamp,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import (
@@ -37,18 +38,40 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.io import re
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
parse_name_field,
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
convert_int_to_bytes,
convert_int_to_hex,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_name_to_oid,
cryptography_serial_number_of_cert,
get_not_valid_after,
get_not_valid_before,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
extract_first_pem,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
parse_name_field,
)
from ansible_collections.community.crypto.plugins.module_utils.time import (
ensure_utc_timezone,
from_epoch_seconds,
get_epoch_seconds,
get_now_datetime,
get_relative_time_option,
UTC,
)
CRYPTOGRAPHY_MINIMAL_VERSION = '1.5'
CRYPTOGRAPHY_ERROR = None
@@ -78,40 +101,6 @@ else:
CRYPTOGRAPHY_ERROR = traceback.format_exc()
if sys.version_info[0] >= 3:
# Python 3 (and newer)
def _count_bytes(n):
return (n.bit_length() + 7) // 8 if n > 0 else 0
def _convert_int_to_bytes(count, no):
return no.to_bytes(count, byteorder='big')
def _pad_hex(n, digits):
res = hex(n)[2:]
if len(res) < digits:
res = '0' * (digits - len(res)) + res
return res
else:
# Python 2
def _count_bytes(n):
if n <= 0:
return 0
h = '%x' % n
return (len(h) + 1) // 2
def _convert_int_to_bytes(count, n):
h = '%x' % n
if len(h) > 2 * count:
raise Exception('Number {1} needs more than {0} bytes!'.format(count, n))
return ('0' * (2 * count - len(h)) + h).decode('hex')
def _pad_hex(n, digits):
h = '%x' % n
if len(h) < digits:
h = '0' * (digits - len(h)) + h
return h
class CryptographyChainMatcher(ChainMatcher):
@staticmethod
def _parse_key_identifier(key_identifier, name, criterium_idx, module):
@@ -197,6 +186,32 @@ class CryptographyBackend(CryptoBackend):
def __init__(self, module):
super(CryptographyBackend, self).__init__(module)
def get_now(self):
return get_now_datetime(with_timezone=CRYPTOGRAPHY_TIMEZONE)
def parse_acme_timestamp(self, timestamp_str):
return _parse_acme_timestamp(timestamp_str, with_timezone=CRYPTOGRAPHY_TIMEZONE)
def parse_module_parameter(self, value, name):
try:
return get_relative_time_option(value, name, backend='cryptography', with_timezone=CRYPTOGRAPHY_TIMEZONE)
except OpenSSLObjectError as exc:
raise BackendException(to_native(exc))
def interpolate_timestamp(self, timestamp_start, timestamp_end, percentage):
start = get_epoch_seconds(timestamp_start)
end = get_epoch_seconds(timestamp_end)
return from_epoch_seconds(start + percentage * (end - start), with_timezone=CRYPTOGRAPHY_TIMEZONE)
def get_utc_datetime(self, *args, **kwargs):
kwargs_ext = dict(kwargs)
if CRYPTOGRAPHY_TIMEZONE and ('tzinfo' not in kwargs_ext and len(args) < 8):
kwargs_ext['tzinfo'] = UTC
result = datetime.datetime(*args, **kwargs_ext)
if CRYPTOGRAPHY_TIMEZONE and ('tzinfo' in kwargs or len(args) >= 8):
result = ensure_utc_timezone(result)
return result
def parse_key(self, key_file=None, key_content=None, passphrase=None):
'''
Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
@@ -223,8 +238,8 @@ class CryptographyBackend(CryptoBackend):
'alg': 'RS256',
'jwk': {
"kty": "RSA",
"e": nopad_b64(_convert_int_to_bytes(_count_bytes(pk.e), pk.e)),
"n": nopad_b64(_convert_int_to_bytes(_count_bytes(pk.n), pk.n)),
"e": nopad_b64(convert_int_to_bytes(pk.e)),
"n": nopad_b64(convert_int_to_bytes(pk.n)),
},
'hash': 'sha256',
}
@@ -260,8 +275,8 @@ class CryptographyBackend(CryptoBackend):
'jwk': {
"kty": "EC",
"crv": curve,
"x": nopad_b64(_convert_int_to_bytes(num_bytes, pk.x)),
"y": nopad_b64(_convert_int_to_bytes(num_bytes, pk.y)),
"x": nopad_b64(convert_int_to_bytes(pk.x, count=num_bytes)),
"y": nopad_b64(convert_int_to_bytes(pk.y, count=num_bytes)),
},
'hash': hashalg,
'point_size': point_size,
@@ -288,8 +303,8 @@ class CryptographyBackend(CryptoBackend):
hashalg = cryptography.hazmat.primitives.hashes.SHA512
ecdsa = cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hashalg())
r, s = cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature(key_data['key_obj'].sign(sign_payload, ecdsa))
rr = _pad_hex(r, 2 * key_data['point_size'])
ss = _pad_hex(s, 2 * key_data['point_size'])
rr = convert_int_to_hex(r, 2 * key_data['point_size'])
ss = convert_int_to_hex(s, 2 * key_data['point_size'])
signature = binascii.unhexlify(rr) + binascii.unhexlify(ss)
return {
@@ -328,31 +343,51 @@ class CryptographyBackend(CryptoBackend):
},
}
def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None):
'''
Return a list of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
The list is deduplicated, and if a CNAME is present, it will be returned
as the first element in the result.
'''
if csr_content is None:
csr_content = read_file(csr_filename)
else:
csr_content = to_bytes(csr_content)
csr = cryptography.x509.load_pem_x509_csr(csr_content, _cryptography_backend)
identifiers = set()
result = []
def add_identifier(identifier):
if identifier in identifiers:
return
identifiers.add(identifier)
result.append(identifier)
for sub in csr.subject:
if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME:
add_identifier(('dns', sub.value))
for extension in csr.extensions:
if extension.oid == cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
for name in extension.value:
if isinstance(name, cryptography.x509.DNSName):
add_identifier(('dns', name.value))
elif isinstance(name, cryptography.x509.IPAddress):
add_identifier(('ip', name.value.compressed))
else:
raise BackendException('Found unsupported SAN identifier {0}'.format(name))
return result
def get_csr_identifiers(self, csr_filename=None, csr_content=None):
'''
Return a set of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
'''
identifiers = set([])
if csr_content is None:
csr_content = read_file(csr_filename)
else:
csr_content = to_bytes(csr_content)
csr = cryptography.x509.load_pem_x509_csr(csr_content, _cryptography_backend)
for sub in csr.subject:
if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME:
identifiers.add(('dns', sub.value))
for extension in csr.extensions:
if extension.oid == cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
for name in extension.value:
if isinstance(name, cryptography.x509.DNSName):
identifiers.add(('dns', name.value))
elif isinstance(name, cryptography.x509.IPAddress):
identifiers.add(('ip', name.value.compressed))
else:
raise BackendException('Found unsupported SAN identifier {0}'.format(name))
return identifiers
return set(self.get_ordered_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content))
def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
'''
@@ -383,11 +418,54 @@ class CryptographyBackend(CryptoBackend):
raise BackendException('Cannot parse certificate {0}: {1}'.format(cert_filename, e))
if now is None:
now = datetime.datetime.now()
return (cert.not_valid_after - now).days
now = self.get_now()
elif CRYPTOGRAPHY_TIMEZONE:
now = ensure_utc_timezone(now)
return (get_not_valid_after(cert) - now).days
def create_chain_matcher(self, criterium):
'''
Given a Criterium object, creates a ChainMatcher object.
'''
return CryptographyChainMatcher(criterium, self.module)
def get_cert_information(self, cert_filename=None, cert_content=None):
'''
Return some information on a X.509 certificate as a CertificateInformation object.
'''
if cert_filename is not None:
cert_content = read_file(cert_filename)
else:
cert_content = to_bytes(cert_content)
# Make sure we have at most one PEM. Otherwise cryptography 36.0.0 will barf.
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:
if cert_filename is None:
raise BackendException('Cannot parse certificate: {0}'.format(e))
raise BackendException('Cannot parse certificate {0}: {1}'.format(cert_filename, e))
ski = None
try:
ext = cert.extensions.get_extension_for_class(cryptography.x509.SubjectKeyIdentifier)
ski = ext.value.digest
except cryptography.x509.ExtensionNotFound:
pass
aki = None
try:
ext = cert.extensions.get_extension_for_class(cryptography.x509.AuthorityKeyIdentifier)
aki = ext.value.key_identifier
except cryptography.x509.ExtensionNotFound:
pass
return CertificateInformation(
not_valid_after=get_not_valid_after(cert),
not_valid_before=get_not_valid_before(cert),
serial_number=cryptography_serial_number_of_cert(cert),
subject_key_identifier=ski,
authority_key_identifier=aki,
)

View File

@@ -20,6 +20,7 @@ import traceback
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
CertificateInformation,
CryptoBackend,
)
@@ -30,6 +31,8 @@ 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.crypto.math import convert_bytes_to_int
try:
import ipaddress
except ImportError:
@@ -39,6 +42,33 @@ except ImportError:
_OPENSSL_ENVIRONMENT_UPDATE = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
def _extract_date(out_text, name, cert_filename_suffix=""):
try:
date_str = re.search(r"\s+%s\s*:\s+(.*)" % name, out_text).group(1)
return datetime.datetime.strptime(date_str, '%b %d %H:%M:%S %Y %Z')
except AttributeError:
raise BackendException("No '{0}' date found{1}".format(name, cert_filename_suffix))
except ValueError as exc:
raise BackendException("Failed to parse '{0}' date{1}: {2}".format(name, cert_filename_suffix, exc))
def _decode_octets(octets_text):
return binascii.unhexlify(re.sub(r"(\s|:)", "", octets_text).encode("utf-8"))
def _extract_octets(out_text, name, required=True, potential_prefixes=None):
regexp = r"\s+%s:\s*\n\s+%s([A-Fa-f0-9]{2}(?::[A-Fa-f0-9]{2})*)\s*\n" % (
name,
('(?:%s)' % '|'.join(re.escape(pp) for pp in potential_prefixes)) if potential_prefixes else '',
)
match = re.search(regexp, out_text, re.MULTILINE | re.DOTALL)
if match is not None:
return _decode_octets(match.group(1))
if not required:
return None
raise BackendException("No '{0}' octet string found".format(name))
class OpenSSLCLIBackend(CryptoBackend):
def __init__(self, module, openssl_binary=None):
super(OpenSSLCLIBackend, self).__init__(module)
@@ -89,10 +119,12 @@ class OpenSSLCLIBackend(CryptoBackend):
dummy, out, dummy = self.module.run_command(
openssl_keydump_cmd, check_rc=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
out_text = to_text(out, errors='surrogate_or_strict')
if account_key_type == 'rsa':
pub_hex, pub_exp = re.search(
r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)",
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL).groups()
pub_hex = re.search(r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent", out_text, re.MULTILINE | re.DOTALL).group(1)
pub_exp = re.search(r"\npublicExponent: ([0-9]+)", out_text, re.MULTILINE | re.DOTALL).group(1)
pub_exp = "{0:x}".format(int(pub_exp))
if len(pub_exp) % 2:
pub_exp = "0{0}".format(pub_exp)
@@ -104,17 +136,19 @@ class OpenSSLCLIBackend(CryptoBackend):
'jwk': {
"kty": "RSA",
"e": nopad_b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
"n": nopad_b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
"n": nopad_b64(_decode_octets(pub_hex)),
},
'hash': 'sha256',
}
elif account_key_type == 'ec':
pub_data = re.search(
r"pub:\s*\n\s+04:([a-f0-9\:\s]+?)\nASN1 OID: (\S+)(?:\nNIST CURVE: (\S+))?",
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL)
out_text,
re.MULTILINE | re.DOTALL,
)
if pub_data is None:
raise KeyParsingError('cannot parse elliptic curve key')
pub_hex = binascii.unhexlify(re.sub(r"(\s|:)", "", pub_data.group(1)).encode("utf-8"))
pub_hex = _decode_octets(pub_data.group(1))
asn1_oid_curve = pub_data.group(2).lower()
nist_curve = pub_data.group(3).lower() if pub_data.group(3) else None
if asn1_oid_curve == 'prime256v1' or nist_curve == 'p-256':
@@ -225,11 +259,14 @@ class OpenSSLCLIBackend(CryptoBackend):
# 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):
def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None):
'''
Return a set of requested identifiers (CN and SANs) for the CSR.
Return a list of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
The list is deduplicated, and if a CNAME is present, it will be returned
as the first element in the result.
'''
filename = csr_filename
data = None
@@ -241,24 +278,40 @@ class OpenSSLCLIBackend(CryptoBackend):
dummy, out, dummy = self.module.run_command(
openssl_csr_cmd, data=data, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
identifiers = set([])
identifiers = set()
result = []
def add_identifier(identifier):
if identifier in identifiers:
return
identifiers.add(identifier)
result.append(identifier)
common_name = re.search(r"Subject:.* CN\s?=\s?([^\s,;/]+)", to_text(out, errors='surrogate_or_strict'))
if common_name is not None:
identifiers.add(('dns', common_name.group(1)))
add_identifier(('dns', common_name.group(1)))
subject_alt_names = re.search(
r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n",
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL)
if subject_alt_names is not None:
for san in subject_alt_names.group(1).split(", "):
if san.lower().startswith("dns:"):
identifiers.add(('dns', san[4:]))
add_identifier(('dns', san[4:]))
elif san.lower().startswith("ip:"):
identifiers.add(('ip', self._normalize_ip(san[3:])))
add_identifier(('ip', self._normalize_ip(san[3:])))
elif san.lower().startswith("ip address:"):
identifiers.add(('ip', self._normalize_ip(san[11:])))
add_identifier(('ip', self._normalize_ip(san[11:])))
else:
raise BackendException('Found unsupported SAN identifier "{0}"'.format(san))
return identifiers
return result
def get_csr_identifiers(self, csr_filename=None, csr_content=None):
'''
Return a set of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
'''
return set(self.get_ordered_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content))
def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
'''
@@ -284,13 +337,8 @@ class OpenSSLCLIBackend(CryptoBackend):
openssl_cert_cmd = [self.openssl_binary, "x509", "-in", filename, "-noout", "-text"]
dummy, out, dummy = self.module.run_command(
openssl_cert_cmd, data=data, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
try:
not_after_str = re.search(r"\s+Not After\s*:\s+(.*)", to_text(out, errors='surrogate_or_strict')).group(1)
not_after = datetime.datetime.strptime(not_after_str, '%b %d %H:%M:%S %Y %Z')
except AttributeError:
raise BackendException("No 'Not after' date found{0}".format(cert_filename_suffix))
except ValueError:
raise BackendException("Failed to parse 'Not after' date{0}".format(cert_filename_suffix))
out_text = to_text(out, errors='surrogate_or_strict')
not_after = _extract_date(out_text, 'Not After', cert_filename_suffix=cert_filename_suffix)
if now is None:
now = datetime.datetime.now()
return (not_after - now).days
@@ -300,3 +348,43 @@ class OpenSSLCLIBackend(CryptoBackend):
Given a Criterium object, creates a ChainMatcher object.
'''
raise BackendException('Alternate chain matching can only be used with the "cryptography" backend.')
def get_cert_information(self, cert_filename=None, cert_content=None):
'''
Return some information on a X.509 certificate as a CertificateInformation object.
'''
filename = cert_filename
data = None
if cert_filename is not None:
cert_filename_suffix = ' in {0}'.format(cert_filename)
else:
filename = '/dev/stdin'
data = to_bytes(cert_content)
cert_filename_suffix = ''
openssl_cert_cmd = [self.openssl_binary, "x509", "-in", filename, "-noout", "-text"]
dummy, out, dummy = self.module.run_command(
openssl_cert_cmd, data=data, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
out_text = to_text(out, errors='surrogate_or_strict')
not_after = _extract_date(out_text, 'Not After', cert_filename_suffix=cert_filename_suffix)
not_before = _extract_date(out_text, 'Not Before', cert_filename_suffix=cert_filename_suffix)
sn = re.search(
r" Serial Number: ([0-9]+)",
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL)
if sn:
serial = int(sn.group(1))
else:
serial = convert_bytes_to_int(_extract_octets(out_text, 'Serial Number', required=True))
ski = _extract_octets(out_text, 'X509v3 Subject Key Identifier', required=False)
aki = _extract_octets(out_text, 'X509v3 Authority Key Identifier', required=False, potential_prefixes=['keyid:', ''])
return CertificateInformation(
not_valid_after=not_after,
not_valid_before=not_before,
serial_number=serial,
subject_key_identifier=ski,
authority_key_identifier=aki,
)

View File

@@ -9,9 +9,78 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
from collections import namedtuple
import abc
import datetime
import re
from ansible.module_utils import six
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
BackendException,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils.time import (
ensure_utc_timezone,
from_epoch_seconds,
get_epoch_seconds,
get_now_datetime,
get_relative_time_option,
remove_timezone,
)
CertificateInformation = namedtuple(
'CertificateInformation',
(
'not_valid_after',
'not_valid_before',
'serial_number',
'subject_key_identifier',
'authority_key_identifier',
),
)
_FRACTIONAL_MATCHER = re.compile(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(|\.\d+)(Z|[+-]\d{2}:?\d{2}.*)$')
def _reduce_fractional_digits(timestamp_str):
"""
Given a RFC 3339 timestamp that includes too many digits for the fractional seconds part, reduces these to at most 6.
"""
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
m = _FRACTIONAL_MATCHER.match(timestamp_str)
if not m:
raise BackendException('Cannot parse ISO 8601 timestamp {0!r}'.format(timestamp_str))
timestamp, fractional, timezone = m.groups()
if len(fractional) > 7:
# Python does not support anything smaller than microseconds
# (Golang supports nanoseconds, Boulder often emits more fractional digits, which Python chokes on)
fractional = fractional[:7]
return '%s%s%s' % (timestamp, fractional, timezone)
def _parse_acme_timestamp(timestamp_str, with_timezone):
"""
Parses a RFC 3339 timestamp.
"""
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
timestamp_str = _reduce_fractional_digits(timestamp_str)
for format in ('%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S.%fZ', '%Y-%m-%dT%H:%M:%S%z', '%Y-%m-%dT%H:%M:%S.%f%z'):
# Note that %z won't work with Python 2... https://stackoverflow.com/a/27829491
try:
result = datetime.datetime.strptime(timestamp_str, format)
except ValueError:
pass
else:
return ensure_utc_timezone(result) if with_timezone else remove_timezone(result)
raise BackendException('Cannot parse ISO 8601 timestamp {0!r}'.format(timestamp_str))
@six.add_metaclass(abc.ABCMeta)
@@ -19,6 +88,30 @@ class CryptoBackend(object):
def __init__(self, module):
self.module = module
def get_now(self):
return get_now_datetime(with_timezone=False)
def parse_acme_timestamp(self, timestamp_str):
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
return _parse_acme_timestamp(timestamp_str, with_timezone=False)
def parse_module_parameter(self, value, name):
try:
return get_relative_time_option(value, name, backend='cryptography', with_timezone=False)
except OpenSSLObjectError as exc:
raise BackendException(to_native(exc))
def interpolate_timestamp(self, timestamp_start, timestamp_end, percentage):
start = get_epoch_seconds(timestamp_start)
end = get_epoch_seconds(timestamp_end)
return from_epoch_seconds(start + percentage * (end - start), with_timezone=False)
def get_utc_datetime(self, *args, **kwargs):
result = datetime.datetime(*args, **kwargs)
if 'tzinfo' in kwargs or len(args) >= 8:
result = remove_timezone(result)
return result
@abc.abstractmethod
def parse_key(self, key_file=None, key_content=None, passphrase=None):
'''
@@ -34,6 +127,23 @@ class CryptoBackend(object):
def create_mac_key(self, alg, key):
'''Create a MAC key.'''
def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None):
'''
Return a list of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
The list is deduplicated, and if a CNAME is present, it will be returned
as the first element in the result.
'''
self.module.deprecate(
"Every backend must override the get_ordered_csr_identifiers() method."
" The default implementation will be removed in 3.0.0 and this method will be marked as `abstractmethod` by then.",
version='3.0.0',
collection_name='community.crypto',
)
return sorted(self.get_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content))
@abc.abstractmethod
def get_csr_identifiers(self, csr_filename=None, csr_content=None):
'''
@@ -57,3 +167,12 @@ class CryptoBackend(object):
'''
Given a Criterium object, creates a ChainMatcher object.
'''
def get_cert_information(self, cert_filename=None, cert_content=None):
'''
Return some information on a X.509 certificate as a CertificateInformation object.
'''
# Not implementing this method in a backend is DEPRECATED and will be
# disallowed in community.crypto 3.0.0. This method will be marked as
# @abstractmethod by then.
raise BackendException('This backend does not support get_cert_information()')

View File

@@ -103,7 +103,7 @@ class Challenge(object):
# https://tools.ietf.org/html/rfc8555#section-8.4
resource = '_acme-challenge'
value = nopad_b64(hashlib.sha256(to_bytes(key_authorization)).digest())
record = (resource + identifier[1:]) if identifier.startswith('*.') else '{0}.{1}'.format(resource, identifier)
record = '{0}.{1}'.format(resource, identifier[2:] if identifier.startswith('*.') else identifier)
return {
'resource': resource,
'resource_value': value,
@@ -283,13 +283,21 @@ class Authorization(object):
return self.status == 'valid'
return self.wait_for_validation(client, challenge_type)
def can_deactivate(self):
'''
Deactivates this authorization.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2
'''
return self.status in ('valid', 'pending')
def deactivate(self, client):
'''
Deactivates this authorization.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2
'''
if self.status != 'valid':
if not self.can_deactivate():
return
authz_deactivate = {
'status': 'deactivated'

View File

@@ -96,10 +96,12 @@ class ACMEProtocolException(ModuleFailException):
extras['http_status'] = code
if code is not None and code >= 400 and content_json is not None and 'type' in content_json:
if 'status' in content_json and content_json['status'] != code:
code = 'status {problem_code} (HTTP status: {http_code})'.format(
code_msg = 'status {problem_code} (HTTP status: {http_code})'.format(
http_code=format_http_status(code), problem_code=content_json['status'])
else:
code = 'status {problem_code}'.format(problem_code=format_http_status(code))
code_msg = 'status {problem_code}'.format(problem_code=format_http_status(code))
if code == -1 and info.get('msg'):
code_msg = 'error: {msg}'.format(msg=info['msg'])
subproblems = content_json.pop('subproblems', None)
add_msg = ' {problem}.'.format(problem=format_error_problem(content_json))
extras['problem'] = content_json
@@ -113,12 +115,14 @@ class ACMEProtocolException(ModuleFailException):
problem=format_error_problem(problem, subproblem_prefix='{0}.'.format(index)),
)
else:
code = 'HTTP status {code}'.format(code=format_http_status(code))
code_msg = 'HTTP status {code}'.format(code=format_http_status(code))
if code == -1 and info.get('msg'):
code_msg = 'error: {msg}'.format(msg=info['msg'])
if content_json is not None:
add_msg = ' The JSON error result: {content}'.format(content=content_json)
elif content is not None:
add_msg = ' The raw error result: {content}'.format(content=to_text(content))
msg = '{msg} for {url} with {code}'.format(msg=msg, url=url, code=format_http_status(code))
msg = '{msg} for {url} with {code}'.format(msg=msg, url=url, code=code_msg)
elif content_json is not None:
add_msg = ' The JSON result: {content}'.format(content=content_json)
elif content is not None:

View File

@@ -32,6 +32,7 @@ class Order(object):
self.identifiers = []
for identifier in data['identifiers']:
self.identifiers.append((identifier['type'], identifier['value']))
self.replaces_cert_id = data.get('replaces')
self.finalize_uri = data.get('finalize')
self.certificate_uri = data.get('certificate')
self.authorization_uris = data['authorizations']
@@ -44,6 +45,7 @@ class Order(object):
self.status = None
self.identifiers = []
self.replaces_cert_id = None
self.finalize_uri = None
self.certificate_uri = None
self.authorization_uris = []
@@ -62,7 +64,7 @@ class Order(object):
return result
@classmethod
def create(cls, client, identifiers):
def create(cls, client, identifiers, replaces_cert_id=None):
'''
Start a new certificate order (ACME v2 protocol).
https://tools.ietf.org/html/rfc8555#section-7.4
@@ -76,6 +78,8 @@ class Order(object):
new_order = {
"identifiers": acme_identifiers
}
if replaces_cert_id is not None:
new_order["replaces"] = replaces_cert_id
result, info = client.send_signed_request(
client.directory['newOrder'], new_order, error_msg='Failed to start new order', expected_status_codes=[201])
return cls.from_json(client, result, info['location'])

View File

@@ -10,6 +10,7 @@ __metaclass__ = type
import base64
import datetime
import re
import textwrap
import traceback
@@ -19,6 +20,10 @@ from ansible.module_utils.six.moves.urllib.parse import unquote
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import convert_int_to_bytes
from ansible_collections.community.crypto.plugins.module_utils.time import get_now_datetime
def nopad_b64(data):
return base64.urlsafe_b64encode(data).decode('utf8').replace("=", "")
@@ -65,8 +70,61 @@ def pem_to_der(pem_filename=None, pem_content=None):
def process_links(info, callback):
'''
Process link header, calls callback for every link header with the URL and relation as options.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
'''
if 'link' in info:
link = info['link']
for url, relation in re.findall(r'<([^>]+)>;\s*rel="(\w+)"', link):
callback(unquote(url), relation)
def parse_retry_after(value, relative_with_timezone=True, now=None):
'''
Parse the value of a Retry-After header and return a timestamp.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
'''
# First try a number of seconds
try:
delta = datetime.timedelta(seconds=int(value))
if now is None:
now = get_now_datetime(relative_with_timezone)
return now + delta
except ValueError:
pass
try:
return datetime.datetime.strptime(value, '%a, %d %b %Y %H:%M:%S GMT')
except ValueError:
pass
raise ValueError('Cannot parse Retry-After header value %s' % repr(value))
def compute_cert_id(
backend,
cert_info=None,
cert_filename=None,
cert_content=None,
none_if_required_information_is_missing=False,
):
# Obtain certificate info if not provided
if cert_info is None:
cert_info = backend.get_cert_information(cert_filename=cert_filename, cert_content=cert_content)
# Convert Authority Key Identifier to string
if cert_info.authority_key_identifier is None:
if none_if_required_information_is_missing:
return None
raise ModuleFailException('Certificate has no Authority Key Identifier extension')
aki = to_native(base64.urlsafe_b64encode(cert_info.authority_key_identifier)).replace('=', '')
# Convert serial number to string
serial_bytes = convert_int_to_bytes(cert_info.serial_number)
if ord(serial_bytes[:1]) >= 128:
serial_bytes = b'\x00' + serial_bytes
serial = to_native(base64.urlsafe_b64encode(serial_bytes)).replace('=', '')
# Compose cert ID
return '{aki}.{serial}'.format(aki=aki, serial=serial)

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule
def _ensure_list(value):
if value is None:
return []
return list(value)
class ArgumentSpec:
def __init__(self, argument_spec=None, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None):
self.argument_spec = argument_spec or {}
self.mutually_exclusive = _ensure_list(mutually_exclusive)
self.required_together = _ensure_list(required_together)
self.required_one_of = _ensure_list(required_one_of)
self.required_if = _ensure_list(required_if)
self.required_by = required_by or {}
def update_argspec(self, **kwargs):
self.argument_spec.update(kwargs)
return self
def update(self, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None):
if mutually_exclusive:
self.mutually_exclusive.extend(mutually_exclusive)
if required_together:
self.required_together.extend(required_together)
if required_one_of:
self.required_one_of.extend(required_one_of)
if required_if:
self.required_if.extend(required_if)
if required_by:
for k, v in required_by.items():
if k in self.required_by:
v = list(self.required_by[k]) + list(v)
self.required_by[k] = v
return self
def merge(self, other):
self.update_argspec(**other.argument_spec)
self.update(
mutually_exclusive=other.mutually_exclusive,
required_together=other.required_together,
required_one_of=other.required_one_of,
required_if=other.required_if,
required_by=other.required_by,
)
return self
def create_ansible_module_helper(self, clazz, args, **kwargs):
return clazz(
*args,
argument_spec=self.argument_spec,
mutually_exclusive=self.mutually_exclusive,
required_together=self.required_together,
required_one_of=self.required_one_of,
required_if=self.required_if,
required_by=self.required_by,
**kwargs)
def create_ansible_module(self, **kwargs):
return self.create_ansible_module_helper(AnsibleModule, (), **kwargs)
__all__ = ('ArgumentSpec', )

View File

@@ -19,6 +19,7 @@ from .basic import (
)
from .cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_decode_name,
)
@@ -27,6 +28,11 @@ from ._obj2txt import (
)
# TODO: once cryptography has a _utc variant of InvalidityDate.invalidity_date, set this
# to True and adjust get_invalidity_date() accordingly.
# (https://github.com/pyca/cryptography/issues/10818)
CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE = False
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
@@ -55,7 +61,7 @@ else:
def cryptography_decode_revoked_certificate(cert):
result = {
'serial_number': cert.serial_number,
'revocation_date': cert.revocation_date,
'revocation_date': get_revocation_date(cert),
'issuer': None,
'issuer_critical': False,
'reason': None,
@@ -77,7 +83,7 @@ def cryptography_decode_revoked_certificate(cert):
pass
try:
ext = cert.extensions.get_extension_for_class(x509.InvalidityDate)
result['invalidity_date'] = ext.value.invalidity_date
result['invalidity_date'] = get_invalidity_date(ext.value)
result['invalidity_date_critical'] = ext.critical
except x509.ExtensionNotFound:
pass
@@ -112,3 +118,38 @@ def cryptography_get_signature_algorithm_oid_from_crl(crl):
crl._x509_crl.sig_alg.algorithm
)
return x509.oid.ObjectIdentifier(dotted)
def get_next_update(obj):
if CRYPTOGRAPHY_TIMEZONE:
return obj.next_update_utc
return obj.next_update
def get_last_update(obj):
if CRYPTOGRAPHY_TIMEZONE:
return obj.last_update_utc
return obj.last_update
def get_revocation_date(obj):
if CRYPTOGRAPHY_TIMEZONE:
return obj.revocation_date_utc
return obj.revocation_date
def get_invalidity_date(obj):
# TODO: special handling if CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE is True
return obj.invalidity_date
def set_next_update(builder, value):
return builder.next_update(value)
def set_last_update(builder, value):
return builder.last_update(value)
def set_revocation_date(builder, value):
return builder.revocation_date(value)

View File

@@ -29,7 +29,9 @@ try:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
import ipaddress
_HAS_CRYPTOGRAPHY = True
except ImportError:
_HAS_CRYPTOGRAPHY = False
# Error handled in the calling module.
pass
@@ -106,6 +108,11 @@ from ._objects import (
from ._obj2txt import obj2txt
CRYPTOGRAPHY_TIMEZONE = False
if _HAS_CRYPTOGRAPHY:
CRYPTOGRAPHY_TIMEZONE = LooseVersion(cryptography.__version__) >= LooseVersion('42.0.0')
DOTTED_OID = re.compile(r'^\d+(?:\.\d+)+$')
@@ -114,7 +121,7 @@ def cryptography_get_extensions_from_cert(cert):
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.
# the extension parsing ourselves.
backend = default_backend()
try:
# For certain old versions of cryptography, backend is a MultiBackend object,
@@ -166,7 +173,7 @@ def cryptography_get_extensions_from_csr(csr):
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.
# the extension parsing ourselves.
backend = default_backend()
try:
# For certain old versions of cryptography, backend is a MultiBackend object,
@@ -807,3 +814,23 @@ def cryptography_verify_certificate_signature(certificate, signer_public_key):
certificate.signature_hash_algorithm,
signer_public_key
)
def get_not_valid_after(obj):
if CRYPTOGRAPHY_TIMEZONE:
return obj.not_valid_after_utc
return obj.not_valid_after
def get_not_valid_before(obj):
if CRYPTOGRAPHY_TIMEZONE:
return obj.not_valid_before_utc
return obj.not_valid_before
def set_not_valid_after(builder, value):
return builder.not_valid_after(value)
def set_not_valid_before(builder, value):
return builder.not_valid_before(value)

View File

@@ -42,9 +42,18 @@ def quick_is_not_prime(n):
that we could not detect quickly whether it is not prime.
'''
if n <= 2:
return True
return n < 2
# The constant in the next line is the product of all primes < 200
if simple_gcd(n, 7799922041683461553249199106329813876687996789903550945093032474868511536164700810) > 1:
prime_product = 7799922041683461553249199106329813876687996789903550945093032474868511536164700810
gcd = simple_gcd(n, prime_product)
if gcd > 1:
if n < 200 and gcd == n:
# Explicitly check for all primes < 200
return n not in (
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83,
89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179,
181, 191, 193, 197, 199,
)
return True
# TODO: maybe do some iterations of Miller-Rabin to increase confidence
# (https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test)
@@ -54,17 +63,111 @@ def quick_is_not_prime(n):
python_version = (sys.version_info[0], sys.version_info[1])
if python_version >= (2, 7) or python_version >= (3, 1):
# Ansible still supports Python 2.6 on remote nodes
def count_bytes(no):
"""
Given an integer, compute the number of bytes necessary to store its absolute value.
"""
no = abs(no)
if no == 0:
return 0
return (no.bit_length() + 7) // 8
def count_bits(no):
"""
Given an integer, compute the number of bits necessary to store its absolute value.
"""
no = abs(no)
if no == 0:
return 0
return no.bit_length()
else:
# Slow, but works
def count_bytes(no):
"""
Given an integer, compute the number of bytes necessary to store its absolute value.
"""
no = abs(no)
count = 0
while no > 0:
no >>= 8
count += 1
return count
def count_bits(no):
"""
Given an integer, compute the number of bits necessary to store its absolute value.
"""
no = abs(no)
count = 0
while no > 0:
no >>= 1
count += 1
return count
if sys.version_info[0] >= 3:
# Python 3 (and newer)
def _convert_int_to_bytes(count, no):
return no.to_bytes(count, byteorder='big')
def _convert_bytes_to_int(data):
return int.from_bytes(data, byteorder='big', signed=False)
def _to_hex(no):
return hex(no)[2:]
else:
# Python 2
def _convert_int_to_bytes(count, n):
if n == 0 and count == 0:
return ''
h = '%x' % n
if len(h) > 2 * count:
raise Exception('Number {1} needs more than {0} bytes!'.format(count, n))
return ('0' * (2 * count - len(h)) + h).decode('hex')
def _convert_bytes_to_int(data):
v = 0
for x in data:
v = (v << 8) | ord(x)
return v
def _to_hex(no):
return '%x' % no
def convert_int_to_bytes(no, count=None):
"""
Convert the absolute value of an integer to a byte string in network byte order.
If ``count`` is provided, it must be sufficiently large so that the integer's
absolute value can be represented with these number of bytes. The resulting byte
string will have length exactly ``count``.
The value zero will be converted to an empty byte string if ``count`` is provided.
"""
no = abs(no)
if count is None:
count = count_bytes(no)
return _convert_int_to_bytes(count, no)
def convert_int_to_hex(no, digits=None):
"""
Convert the absolute value of an integer to a string of hexadecimal digits.
If ``digits`` is provided, the string will be padded on the left with ``0``s so
that the returned value has length ``digits``. If ``digits`` is not sufficient,
the string will be longer.
"""
no = abs(no)
value = _to_hex(no)
if digits is not None and len(value) < digits:
value = '0' * (digits - len(value)) + value
return value
def convert_bytes_to_int(data):
"""
Convert a byte string to an unsigned integer in network byte order.
"""
return _convert_bytes_to_int(data)

View File

@@ -15,9 +15,9 @@ import traceback
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.argspec import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
@@ -32,6 +32,8 @@ 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,
get_not_valid_after,
get_not_valid_before,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
@@ -251,12 +253,12 @@ class CertificateBackend(object):
# Check not before
if not_before is not None and not self.ignore_timestamps:
if self.existing_certificate.not_valid_before != not_before:
if get_not_valid_before(self.existing_certificate) != not_before:
return True
# Check not after
if not_after is not None and not self.ignore_timestamps:
if self.existing_certificate.not_valid_after != not_after:
if get_not_valid_after(self.existing_certificate) != not_after:
return True
return False

View File

@@ -10,7 +10,6 @@ __metaclass__ = type
import datetime
import time
import os
from ansible.module_utils.common.text.converters import to_native, to_bytes
@@ -19,11 +18,12 @@ from ansible_collections.community.crypto.plugins.module_utils.ecs.api import EC
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_certificate,
get_relative_time_option,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_serial_number_of_cert,
get_not_valid_after,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
@@ -32,6 +32,11 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
CertificateProvider,
)
from ansible_collections.community.crypto.plugins.module_utils.time import (
get_now_datetime,
get_relative_time_option,
)
try:
from cryptography.x509.oid import NameOID
except ImportError:
@@ -42,7 +47,12 @@ class EntrustCertificateBackend(CertificateBackend):
def __init__(self, module, backend):
super(EntrustCertificateBackend, self).__init__(module, backend)
self.trackingId = None
self.notAfter = get_relative_time_option(module.params['entrust_not_after'], 'entrust_not_after', backend=self.backend)
self.notAfter = get_relative_time_option(
module.params['entrust_not_after'],
'entrust_not_after',
backend=self.backend,
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
if self.csr_content is None and self.csr_path is None:
raise CertificateError(
@@ -99,7 +109,7 @@ class EntrustCertificateBackend(CertificateBackend):
# Handle expiration (30 days if not specified)
expiry = self.notAfter
if not expiry:
gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime()))
gmt_now = get_now_datetime(with_timezone=CRYPTOGRAPHY_TIMEZONE)
expiry = gmt_now + datetime.timedelta(days=365)
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
@@ -154,7 +164,7 @@ class EntrustCertificateBackend(CertificateBackend):
expiry = None
if self.backend == 'cryptography':
serial_number = "{0:X}".format(cryptography_serial_number_of_cert(self.existing_certificate))
expiry = self.existing_certificate.not_valid_after
expiry = get_not_valid_after(self.existing_certificate)
# get some information about the expiry of this certificate
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")

View File

@@ -12,7 +12,6 @@ __metaclass__ = type
import abc
import binascii
import datetime
import traceback
from ansible.module_utils import six
@@ -27,16 +26,23 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_decode_name,
cryptography_get_extensions_from_cert,
cryptography_oid_to_name,
cryptography_serial_number_of_cert,
get_not_valid_after,
get_not_valid_before,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
get_publickey_info,
)
from ansible_collections.community.crypto.plugins.module_utils.time import (
get_now_datetime,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
CRYPTOGRAPHY_IMP_ERR = None
@@ -169,7 +175,7 @@ class CertificateInfoRetrieval(object):
not_after = self.get_not_after()
result['not_before'] = not_before.strftime(TIMESTAMP_FORMAT)
result['not_after'] = not_after.strftime(TIMESTAMP_FORMAT)
result['expired'] = not_after < datetime.datetime.utcnow()
result['expired'] = not_after < get_now_datetime(with_timezone=CRYPTOGRAPHY_TIMEZONE)
result['public_key'] = to_native(self._get_public_key_pem())
@@ -322,10 +328,10 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
return None, False
def get_not_before(self):
return self.cert.not_valid_before
return get_not_valid_before(self.cert)
def get_not_after(self):
return self.cert.not_valid_after
return get_not_valid_after(self.cert)
def _get_public_key_pem(self):
return self.cert.public_key().public_bytes(

View File

@@ -22,15 +22,19 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.basic impo
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
load_privatekey,
load_certificate,
get_relative_time_option,
select_message_digest,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_compare_public_keys,
cryptography_key_needs_digest_for_signing,
cryptography_serial_number_of_cert,
cryptography_verify_certificate_signature,
get_not_valid_after,
get_not_valid_before,
set_not_valid_after,
set_not_valid_before,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
@@ -40,6 +44,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
CertificateProvider,
)
from ansible_collections.community.crypto.plugins.module_utils.time import (
get_relative_time_option,
)
try:
import cryptography
from cryptography import x509
@@ -55,8 +63,18 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
self.create_subject_key_identifier = module.params['ownca_create_subject_key_identifier']
self.create_authority_key_identifier = module.params['ownca_create_authority_key_identifier']
self.notBefore = get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend)
self.notAfter = get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend)
self.notBefore = get_relative_time_option(
module.params['ownca_not_before'],
'ownca_not_before',
backend=self.backend,
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
self.notAfter = get_relative_time_option(
module.params['ownca_not_after'],
'ownca_not_after',
backend=self.backend,
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
self.digest = select_message_digest(module.params['ownca_digest'])
self.version = module.params['ownca_version']
self.serial_number = x509.random_serial_number()
@@ -120,8 +138,8 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
cert_builder = cert_builder.subject_name(self.csr.subject)
cert_builder = cert_builder.issuer_name(self.ca_cert.subject)
cert_builder = cert_builder.serial_number(self.serial_number)
cert_builder = cert_builder.not_valid_before(self.notBefore)
cert_builder = cert_builder.not_valid_after(self.notAfter)
cert_builder = set_not_valid_before(cert_builder, self.notBefore)
cert_builder = set_not_valid_after(cert_builder, self.notAfter)
cert_builder = cert_builder.public_key(self.csr.public_key())
has_ski = False
for extension in self.csr.extensions:
@@ -220,8 +238,8 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
if self.cert is None:
self.cert = self.existing_certificate
result.update({
'notBefore': self.cert.not_valid_before.strftime("%Y%m%d%H%M%SZ"),
'notAfter': self.cert.not_valid_after.strftime("%Y%m%d%H%M%SZ"),
'notBefore': get_not_valid_before(self.cert).strftime("%Y%m%d%H%M%SZ"),
'notAfter': get_not_valid_after(self.cert).strftime("%Y%m%d%H%M%SZ"),
'serial_number': cryptography_serial_number_of_cert(self.cert),
})

View File

@@ -14,14 +14,18 @@ import os
from random import randrange
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
get_relative_time_option,
select_message_digest,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_key_needs_digest_for_signing,
cryptography_serial_number_of_cert,
cryptography_verify_certificate_signature,
get_not_valid_after,
get_not_valid_before,
set_not_valid_after,
set_not_valid_before,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
@@ -30,6 +34,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
CertificateProvider,
)
from ansible_collections.community.crypto.plugins.module_utils.time import (
get_relative_time_option,
)
try:
import cryptography
from cryptography import x509
@@ -44,8 +52,18 @@ class SelfSignedCertificateBackendCryptography(CertificateBackend):
super(SelfSignedCertificateBackendCryptography, self).__init__(module, 'cryptography')
self.create_subject_key_identifier = module.params['selfsigned_create_subject_key_identifier']
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.notBefore = get_relative_time_option(
module.params['selfsigned_not_before'],
'selfsigned_not_before',
backend=self.backend,
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
self.notAfter = get_relative_time_option(
module.params['selfsigned_not_after'],
'selfsigned_not_after',
backend=self.backend,
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
self.digest = select_message_digest(module.params['selfsigned_digest'])
self.version = module.params['selfsigned_version']
self.serial_number = x509.random_serial_number()
@@ -95,8 +113,8 @@ class SelfSignedCertificateBackendCryptography(CertificateBackend):
cert_builder = cert_builder.subject_name(self.csr.subject)
cert_builder = cert_builder.issuer_name(self.csr.subject)
cert_builder = cert_builder.serial_number(self.serial_number)
cert_builder = cert_builder.not_valid_before(self.notBefore)
cert_builder = cert_builder.not_valid_after(self.notAfter)
cert_builder = set_not_valid_before(cert_builder, self.notBefore)
cert_builder = set_not_valid_after(cert_builder, self.notAfter)
cert_builder = cert_builder.public_key(self.privatekey.public_key())
has_ski = False
for extension in self.csr.extensions:
@@ -154,8 +172,8 @@ class SelfSignedCertificateBackendCryptography(CertificateBackend):
if self.cert is None:
self.cert = self.existing_certificate
result.update({
'notBefore': self.cert.not_valid_before.strftime("%Y%m%d%H%M%SZ"),
'notAfter': self.cert.not_valid_after.strftime("%Y%m%d%H%M%SZ"),
'notBefore': get_not_valid_before(self.cert).strftime("%Y%m%d%H%M%SZ"),
'notAfter': get_not_valid_after(self.cert).strftime("%Y%m%d%H%M%SZ"),
'serial_number': cryptography_serial_number_of_cert(self.cert),
})

View File

@@ -10,26 +10,19 @@ __metaclass__ = type
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec as _ArgumentSpec
class ArgumentSpec:
def __init__(self, argument_spec, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None):
self.argument_spec = argument_spec
self.mutually_exclusive = mutually_exclusive or []
self.required_together = required_together or []
self.required_one_of = required_one_of or []
self.required_if = required_if or []
self.required_by = required_by or {}
class ArgumentSpec(_ArgumentSpec):
def create_ansible_module_helper(self, clazz, args, **kwargs):
return clazz(
*args,
argument_spec=self.argument_spec,
mutually_exclusive=self.mutually_exclusive,
required_together=self.required_together,
required_one_of=self.required_one_of,
required_if=self.required_if,
required_by=self.required_by,
**kwargs)
result = super(ArgumentSpec, self).create_ansible_module_helper(clazz, args, **kwargs)
result.deprecate(
"The crypto.module_backends.common module utils is deprecated and will be removed from community.crypto 3.0.0."
" Use the argspec module utils from community.crypto instead.",
version='3.0.0',
collection_name='community.crypto',
)
return result
def create_ansible_module(self, **kwargs):
return self.create_ansible_module_helper(AnsibleModule, (), **kwargs)
__all__ = ('AnsibleModule', 'ArgumentSpec')

View File

@@ -17,6 +17,8 @@ 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
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
@@ -49,8 +51,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
get_csr_info,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'

View File

@@ -17,6 +17,8 @@ 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.argspec import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
@@ -42,8 +44,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
get_privatekey_info,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'

View File

@@ -15,12 +15,14 @@ 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.argspec import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.io import (
load_file,
)
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
CRYPTOGRAPHY_HAS_X25519,
CRYPTOGRAPHY_HAS_X448,
@@ -37,8 +39,6 @@ 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'
@@ -106,7 +106,7 @@ class PrivateKeyConvertBackend:
@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)."""
"""Check whether data can be loaded as a private key with the provided passphrase. Return tuple (type, private_key)."""
pass
def needs_conversion(self):

View File

@@ -105,9 +105,12 @@ def _check_dsa_consistency(key_public_data, key_private_data):
return True
def _is_cryptography_key_consistent(key, key_public_data, key_private_data):
def _is_cryptography_key_consistent(key, key_public_data, key_private_data, warn_func=None):
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
return bool(key._backend._lib.RSA_check_key(key._rsa_cdata))
# key._backend was removed in cryptography 42.0.0
backend = getattr(key, '_backend', None)
if backend is not None:
return bool(backend._lib.RSA_check_key(key._rsa_cdata))
if isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
result = _check_dsa_consistency(key_public_data, key_private_data)
if result is not None:
@@ -157,6 +160,8 @@ def _is_cryptography_key_consistent(key, key_public_data, key_private_data):
except cryptography.exceptions.InvalidSignature:
return False
# For X25519 and X448, there's no test yet.
if warn_func is not None:
warn_func('Cannot determine consistency for key of type %s' % type(key))
return None
@@ -253,7 +258,7 @@ class PrivateKeyInfoRetrievalCryptography(PrivateKeyInfoRetrieval):
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)
return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data, warn_func=self.module.warn)
def get_privatekey_info(module, backend, content, passphrase=None, return_private_key_data=False, prefer_one_fingerprint=False):

View File

@@ -9,6 +9,7 @@ __metaclass__ = type
PEM_START = '-----BEGIN '
PEM_END_START = '-----END '
PEM_END = '-----'
PKCS8_PRIVATEKEY_NAMES = ('PRIVATE KEY', 'ENCRYPTED PRIVATE KEY')
PKCS1_PRIVATEKEY_SUFFIX = ' PRIVATE KEY'
@@ -77,3 +78,31 @@ def extract_first_pem(text):
if not all_pems:
return None
return all_pems[0]
def _extract_type(line, start=PEM_START):
if not line.startswith(start):
return None
if not line.endswith(PEM_END):
return None
return line[len(start):-len(PEM_END)]
def extract_pem(content, strict=False):
lines = content.splitlines()
if len(lines) < 3:
raise ValueError('PEM must have at least 3 lines, have only {count}'.format(count=len(lines)))
header_type = _extract_type(lines[0])
if header_type is None:
raise ValueError('First line is not of format {start}...{end}: {line!r}'.format(start=PEM_START, end=PEM_END, line=lines[0]))
footer_type = _extract_type(lines[-1], start=PEM_END_START)
if strict:
if header_type != footer_type:
raise ValueError('Header type ({header}) is different from footer type ({footer})'.format(header=header_type, footer=footer_type))
for idx, line in enumerate(lines[1:-2]):
if len(line) != 64:
raise ValueError('Line {idx} has length {len} instead of 64'.format(idx=idx, len=len(line)))
if not (0 < len(lines[-2]) <= 64):
raise ValueError('Last line has length {len}, should be in (0, 64]'.format(len=len(lines[-2])))
content = lines[1:-1]
return header_type, ''.join(content)

View File

@@ -9,19 +9,25 @@ __metaclass__ = type
import abc
import datetime
import errno
import hashlib
import os
import re
from ansible.module_utils import six
from ansible.module_utils.common.text.converters import to_native, to_bytes
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
identify_pem_format,
)
from ansible_collections.community.crypto.plugins.module_utils.time import ( # noqa: F401, pylint: disable=unused-import
# These imports are for backwards compatibility
get_now_datetime,
ensure_utc_timezone,
convert_relative_to_datetime,
get_relative_time_option,
)
try:
from OpenSSL import crypto
HAS_PYOPENSSL = True
@@ -279,69 +285,6 @@ def parse_ordered_name_field(input_list, name_field_name):
return result
def convert_relative_to_datetime(relative_time_string):
"""Get a datetime.datetime or None from a string in the time format described in sshd_config(5)"""
parsed_result = re.match(
r"^(?P<prefix>[+-])((?P<weeks>\d+)[wW])?((?P<days>\d+)[dD])?((?P<hours>\d+)[hH])?((?P<minutes>\d+)[mM])?((?P<seconds>\d+)[sS]?)?$",
relative_time_string)
if parsed_result is None or len(relative_time_string) == 1:
# not matched or only a single "+" or "-"
return None
offset = datetime.timedelta(0)
if parsed_result.group("weeks") is not None:
offset += datetime.timedelta(weeks=int(parsed_result.group("weeks")))
if parsed_result.group("days") is not None:
offset += datetime.timedelta(days=int(parsed_result.group("days")))
if parsed_result.group("hours") is not None:
offset += datetime.timedelta(hours=int(parsed_result.group("hours")))
if parsed_result.group("minutes") is not None:
offset += datetime.timedelta(
minutes=int(parsed_result.group("minutes")))
if parsed_result.group("seconds") is not None:
offset += datetime.timedelta(
seconds=int(parsed_result.group("seconds")))
if parsed_result.group("prefix") == "+":
return datetime.datetime.utcnow() + offset
else:
return datetime.datetime.utcnow() - offset
def get_relative_time_option(input_string, input_name, backend='cryptography'):
"""Return an absolute timespec if a relative timespec or an ASN1 formatted
string is provided.
The return value will be a datetime object for the cryptography backend,
and a ASN1 formatted string for the pyopenssl backend."""
result = to_native(input_string)
if result is None:
raise OpenSSLObjectError(
'The timespec "%s" for %s is not valid' %
input_string, input_name)
# Relative time
if result.startswith("+") or result.startswith("-"):
result_datetime = convert_relative_to_datetime(result)
if backend == 'pyopenssl':
return result_datetime.strftime("%Y%m%d%H%M%SZ")
elif backend == 'cryptography':
return result_datetime
# Absolute time
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)
except ValueError:
pass
raise OpenSSLObjectError(
'The time spec "%s" for %s is invalid' %
(input_string, input_name)
)
def select_message_digest(digest_string):
digest = None
if digest_string == 'sha256':

View File

@@ -22,18 +22,24 @@ __metaclass__ = type
import abc
import binascii
import datetime as _datetime
import os
import sys
from base64 import b64encode
from datetime import datetime
from hashlib import sha256
from ansible.module_utils import six
from ansible.module_utils.common.text.converters import to_text
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import convert_relative_to_datetime
from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
OpensshParser,
_OpensshWriter,
)
from ansible_collections.community.crypto.plugins.module_utils.time import (
add_or_remove_timezone as _add_or_remove_timezone,
convert_relative_to_datetime,
UTC as _UTC,
)
# See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
_USER_TYPE = 1
@@ -61,8 +67,11 @@ _ECDSA_CURVE_IDENTIFIERS_LOOKUP = {
b'nistp521': 'ecdsa-nistp521',
}
_ALWAYS = datetime(1970, 1, 1)
_FOREVER = datetime.max
_USE_TIMEZONE = sys.version_info >= (3, 6)
_ALWAYS = _add_or_remove_timezone(datetime(1970, 1, 1), with_timezone=_USE_TIMEZONE)
_FOREVER = datetime(9999, 12, 31, 23, 59, 59, 999999, _UTC) if _USE_TIMEZONE else datetime.max
_CRITICAL_OPTIONS = (
'force-command',
@@ -136,7 +145,7 @@ class OpensshCertificateTimeParameters(object):
elif dt == _FOREVER:
result = 'forever'
else:
result = dt.isoformat() if date_format == 'human_readable' else dt.strftime("%Y%m%d%H%M%S")
result = dt.isoformat().replace('+00:00', '') if date_format == 'human_readable' else dt.strftime("%Y%m%d%H%M%S")
elif date_format == 'timestamp':
td = dt - _ALWAYS
result = int((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6)
@@ -167,7 +176,10 @@ class OpensshCertificateTimeParameters(object):
result = _FOREVER
else:
try:
result = datetime.utcfromtimestamp(timestamp)
if _USE_TIMEZONE:
result = datetime.fromtimestamp(timestamp, tz=_datetime.timezone.utc)
else:
result = datetime.utcfromtimestamp(timestamp)
except OverflowError as e:
raise ValueError
return result
@@ -180,11 +192,11 @@ class OpensshCertificateTimeParameters(object):
elif time_string == 'forever':
result = _FOREVER
elif is_relative_time_string(time_string):
result = convert_relative_to_datetime(time_string)
result = convert_relative_to_datetime(time_string, with_timezone=_USE_TIMEZONE)
else:
for time_format in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
try:
result = datetime.strptime(time_string, time_format)
result = _add_or_remove_timezone(datetime.strptime(time_string, time_format), with_timezone=_USE_TIMEZONE)
except ValueError:
pass
if result is None:

View File

@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
convert_int_to_hex,
)
def th(number):
abs_number = abs(number)
mod_10 = abs_number % 10
mod_100 = abs_number % 100
if mod_100 not in (11, 12, 13):
if mod_10 == 1:
return 'st'
if mod_10 == 2:
return 'nd'
if mod_10 == 3:
return 'rd'
return 'th'
def parse_serial(value):
"""
Given a colon-separated string of hexadecimal byte values, converts it to an integer.
"""
value = to_native(value)
result = 0
for i, part in enumerate(value.split(':')):
try:
part_value = int(part, 16)
if part_value < 0 or part_value > 255:
raise ValueError('the value is not in range [0, 255]')
except ValueError as exc:
raise ValueError("The {idx}{th} part {part!r} is not a hexadecimal number in range [0, 255]: {exc}".format(
idx=i + 1, th=th(i + 1), part=part, exc=exc))
result = (result << 8) | part_value
return result
def to_serial(value):
"""
Given an integer, converts its absolute value to a colon-separated string of hexadecimal byte values.
"""
value = convert_int_to_hex(value).upper()
if len(value) % 2 != 0:
value = '0' + value
return ':'.join(value[i:i + 2] for i in range(0, len(value), 2))

View File

@@ -0,0 +1,171 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import datetime
import re
import sys
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
)
try:
UTC = datetime.timezone.utc
except AttributeError:
_DURATION_ZERO = datetime.timedelta(0)
class _UTCClass(datetime.tzinfo):
def utcoffset(self, dt):
return _DURATION_ZERO
def dst(self, dt):
return _DURATION_ZERO
def tzname(self, dt):
return 'UTC'
def fromutc(self, dt):
return dt
def __repr__(self):
return 'UTC'
UTC = _UTCClass()
def get_now_datetime(with_timezone):
if with_timezone:
return datetime.datetime.now(tz=UTC)
return datetime.datetime.utcnow()
def ensure_utc_timezone(timestamp):
if timestamp.tzinfo is UTC:
return timestamp
if timestamp.tzinfo is None:
# We assume that naive datetime objects use timezone UTC!
return timestamp.replace(tzinfo=UTC)
return timestamp.astimezone(UTC)
def remove_timezone(timestamp):
# Convert to native datetime object
if timestamp.tzinfo is None:
return timestamp
if timestamp.tzinfo is not UTC:
timestamp = timestamp.astimezone(UTC)
return timestamp.replace(tzinfo=None)
def add_or_remove_timezone(timestamp, with_timezone):
return ensure_utc_timezone(timestamp) if with_timezone else remove_timezone(timestamp)
if sys.version_info < (3, 3):
def get_epoch_seconds(timestamp):
epoch = datetime.datetime(1970, 1, 1, tzinfo=UTC if timestamp.tzinfo is not None else None)
delta = timestamp - epoch
try:
return delta.total_seconds()
except AttributeError:
# Python 2.6 and earlier: total_seconds() does not yet exist, so we use the formula from
# https://docs.python.org/2/library/datetime.html#datetime.timedelta.total_seconds
return (delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10**6) / 10**6
else:
def get_epoch_seconds(timestamp):
return timestamp.timestamp()
def from_epoch_seconds(timestamp, with_timezone):
if with_timezone:
return datetime.datetime.fromtimestamp(timestamp, UTC)
return datetime.datetime.utcfromtimestamp(timestamp)
def convert_relative_to_datetime(relative_time_string, with_timezone=False, now=None):
"""Get a datetime.datetime or None from a string in the time format described in sshd_config(5)"""
parsed_result = re.match(
r"^(?P<prefix>[+-])((?P<weeks>\d+)[wW])?((?P<days>\d+)[dD])?((?P<hours>\d+)[hH])?((?P<minutes>\d+)[mM])?((?P<seconds>\d+)[sS]?)?$",
relative_time_string)
if parsed_result is None or len(relative_time_string) == 1:
# not matched or only a single "+" or "-"
return None
offset = datetime.timedelta(0)
if parsed_result.group("weeks") is not None:
offset += datetime.timedelta(weeks=int(parsed_result.group("weeks")))
if parsed_result.group("days") is not None:
offset += datetime.timedelta(days=int(parsed_result.group("days")))
if parsed_result.group("hours") is not None:
offset += datetime.timedelta(hours=int(parsed_result.group("hours")))
if parsed_result.group("minutes") is not None:
offset += datetime.timedelta(
minutes=int(parsed_result.group("minutes")))
if parsed_result.group("seconds") is not None:
offset += datetime.timedelta(
seconds=int(parsed_result.group("seconds")))
if now is None:
now = get_now_datetime(with_timezone=with_timezone)
else:
now = add_or_remove_timezone(now, with_timezone=with_timezone)
if parsed_result.group("prefix") == "+":
return now + offset
else:
return now - offset
def get_relative_time_option(input_string, input_name, backend='cryptography', with_timezone=False, now=None):
"""Return an absolute timespec if a relative timespec or an ASN1 formatted
string is provided.
The return value will be a datetime object for the cryptography backend,
and a ASN1 formatted string for the pyopenssl backend."""
result = to_native(input_string)
if result is None:
raise OpenSSLObjectError(
'The timespec "%s" for %s is not valid' %
input_string, input_name)
# Relative time
if result.startswith("+") or result.startswith("-"):
result_datetime = convert_relative_to_datetime(result, with_timezone=with_timezone, now=now)
if backend == 'pyopenssl':
return result_datetime.strftime("%Y%m%d%H%M%SZ")
elif backend == 'cryptography':
return result_datetime
# Absolute time
if backend == 'pyopenssl':
return input_string
elif backend == 'cryptography':
for date_fmt, length in [
('%Y%m%d%H%M%SZ', 15), # this also parses '202401020304Z', but as datetime(2024, 1, 2, 3, 0, 4)
('%Y%m%d%H%MZ', 13),
('%Y%m%d%H%M%S%z', 14 + 5), # this also parses '202401020304+0000', but as datetime(2024, 1, 2, 3, 0, 4, tzinfo=...)
('%Y%m%d%H%M%z', 12 + 5),
]:
if len(result) != length:
continue
try:
res = datetime.datetime.strptime(result, date_fmt)
except ValueError:
pass
else:
return add_or_remove_timezone(res, with_timezone=with_timezone)
raise OpenSSLObjectError(
'The time spec "%s" for %s is invalid' %
(input_string, input_name)
)

View File

@@ -37,7 +37,8 @@ seealso:
- module: community.crypto.acme_inspect
description: Allows to debug problems.
extends_documentation_fragment:
- community.crypto.acme
- community.crypto.acme.basic
- community.crypto.acme.account
- community.crypto.attributes
- community.crypto.attributes.actiongroup_acme
attributes:
@@ -169,11 +170,9 @@ account_uri:
import base64
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
get_default_argspec,
create_default_argspec,
ACMEClient,
)
@@ -188,8 +187,8 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
def main():
argument_spec = get_default_argspec()
argument_spec.update(dict(
argument_spec = create_default_argspec()
argument_spec.update_argspec(
terms_agreed=dict(type='bool', default=False),
state=dict(type='str', required=True, choices=['absent', 'present', 'changed_key']),
allow_creation=dict(type='bool', default=True),
@@ -202,14 +201,9 @@ def main():
alg=dict(type='str', required=True, choices=['HS256', 'HS384', 'HS512']),
key=dict(type='str', required=True, no_log=True),
))
))
module = AnsibleModule(
argument_spec=argument_spec,
required_one_of=(
['account_key_src', 'account_key_content'],
),
)
argument_spec.update(
mutually_exclusive=(
['account_key_src', 'account_key_content'],
['new_account_key_src', 'new_account_key_content'],
),
required_if=(
@@ -217,8 +211,8 @@ def main():
# new_account_key_src and new_account_key_content are specified
['state', 'changed_key', ['new_account_key_src', 'new_account_key_content'], True],
),
supports_check_mode=True,
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True)
if module.params['external_account_binding']:

View File

@@ -25,7 +25,8 @@ notes:
- "This module was called C(acme_account_facts) before Ansible 2.8. The usage
did not change."
extends_documentation_fragment:
- community.crypto.acme
- community.crypto.acme.basic
- community.crypto.acme.account
- community.crypto.attributes
- community.crypto.attributes.actiongroup_acme
- community.crypto.attributes.info_module
@@ -213,11 +214,9 @@ order_uris:
version_added: 1.5.0
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
get_default_argspec,
create_default_argspec,
ACMEClient,
)
@@ -270,20 +269,11 @@ def get_order(client, order_url):
def main():
argument_spec = get_default_argspec()
argument_spec.update(dict(
argument_spec = create_default_argspec()
argument_spec.update_argspec(
retrieve_orders=dict(type='str', default='ignore', choices=['ignore', 'url_list', 'object_list']),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_one_of=(
['account_key_src', 'account_key_content'],
),
mutually_exclusive=(
['account_key_src', 'account_key_content'],
),
supports_check_mode=True,
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True)
try:

View File

@@ -0,0 +1,142 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2018 Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: acme_ari_info
author: "Felix Fontein (@felixfontein)"
version_added: 2.20.0
short_description: Retrieves ACME Renewal Information (ARI) for a certificate
description:
- "Allows to retrieve renewal information on a certificate obtained with the
L(ACME protocol,https://tools.ietf.org/html/rfc8555)."
- "This module only works with the ACME v2 protocol, and requires the ACME server
to support the ARI extension (U(https://datatracker.ietf.org/doc/draft-ietf-acme-ari/)).
This module implements version 3 of the ARI draft."
extends_documentation_fragment:
- community.crypto.acme.basic
- community.crypto.acme.no_account
- community.crypto.attributes
- community.crypto.attributes.info_module
options:
certificate_path:
description:
- A path to the X.509 certificate to request information for.
- Exactly one of O(certificate_path) and O(certificate_content) must be provided.
type: path
certificate_content:
description:
- The content of the X.509 certificate to request information for.
- Exactly one of O(certificate_path) and O(certificate_content) must be provided.
type: str
seealso:
- module: community.crypto.acme_certificate
description: Allows to obtain a certificate using the ACME protocol
- module: community.crypto.acme_certificate_revoke
description: Allows to revoke a certificate using the ACME protocol
'''
EXAMPLES = '''
- name: Retrieve renewal information for a certificate
community.crypto.acme_ari_info:
certificate_path: /etc/httpd/ssl/sample.com.crt
register: cert_data
- name: Show the certificate renewal information
ansible.builtin.debug:
var: cert_data.renewal_info
'''
RETURN = '''
renewal_info:
description: The ARI renewal info object (U(https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-4.2)).
returned: success
type: dict
contains:
suggestedWindow:
description:
- Describes the window during which the certificate should be renewed.
type: dict
returned: always
contains:
start:
description:
- The start of the window during which the certificate should be renewed.
- The format is specified in L(RFC 3339,https://www.rfc-editor.org/info/rfc3339).
returned: always
type: str
sample: '2021-01-03T00:00:00Z'
end:
description:
- The end of the window during which the certificate should be renewed.
- The format is specified in L(RFC 3339,https://www.rfc-editor.org/info/rfc3339).
returned: always
type: str
sample: '2021-01-03T00:00:00Z'
explanationURL:
description:
- A URL pointing to a page which may explain why the suggested renewal window is what it is.
- For example, it may be a page explaining the CA's dynamic load-balancing strategy, or a
page documenting which certificates are affected by a mass revocation event. Should be shown
to the user.
returned: depends on the ACME server
type: str
sample: https://example.com/docs/ari
retryAfter:
description:
- A timestamp before the next retry to ask for this information should not be made.
returned: depends on the ACME server
type: str
sample: '2024-04-29T01:17:10.236921+00:00'
'''
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
create_default_argspec,
ACMEClient,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
def main():
argument_spec = create_default_argspec(with_account=False)
argument_spec.update_argspec(
certificate_path=dict(type='path'),
certificate_content=dict(type='str'),
)
argument_spec.update(
required_one_of=(
['certificate_path', 'certificate_content'],
),
mutually_exclusive=(
['certificate_path', 'certificate_content'],
),
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True)
try:
client = ACMEClient(module, backend)
if not client.directory.has_renewal_info_endpoint():
module.fail_json(msg='The ACME endpoint does not support ACME Renewal Information retrieval')
renewal_info = client.get_renewal_info(
cert_filename=module.params['certificate_path'],
cert_content=module.params['certificate_content'],
include_retry_after=True,
)
module.exit_json(renewal_info=renewal_info)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
main()

View File

@@ -58,7 +58,7 @@ seealso:
link: https://tools.ietf.org/html/rfc8555
- name: ACME TLS ALPN Challenge Extension
description: The specification of the V(tls-alpn-01) challenge (RFC 8737).
link: https://www.rfc-editor.org/rfc/rfc8737.html-05
link: https://www.rfc-editor.org/rfc/rfc8737.html
- module: community.crypto.acme_challenge_cert_helper
description: Helps preparing V(tls-alpn-01) challenges.
- module: community.crypto.openssl_privatekey
@@ -77,8 +77,12 @@ seealso:
description: Allows to create, modify or delete an ACME account.
- module: community.crypto.acme_inspect
description: Allows to debug problems.
- module: community.crypto.acme_certificate_deactivate_authz
description: Allows to deactivate (invalidate) ACME v2 orders.
extends_documentation_fragment:
- community.crypto.acme
- community.crypto.acme.basic
- community.crypto.acme.account
- community.crypto.acme.certificate
- community.crypto.attributes
- community.crypto.attributes.files
- community.crypto.attributes.actiongroup_acme
@@ -138,32 +142,8 @@ options:
- 'tls-alpn-01'
- 'no challenge'
csr:
description:
- "File containing the CSR for the new certificate."
- "Can be created with M(community.crypto.openssl_csr) or C(openssl req ...)."
- "The CSR may contain multiple Subject Alternate Names, but each one
will lead to an individual challenge that must be fulfilled for the
CSR to be signed."
- "I(Note): the private key used to create the CSR I(must not) be the
account key. This is a bad idea from a security point of view, and
the CA should not accept the CSR. The ACME server should return an
error in this case."
- Precisely one of O(csr) or O(csr_content) must be specified.
type: path
aliases: ['src']
csr_content:
description:
- "Content of the CSR for the new certificate."
- "Can be created with M(community.crypto.openssl_csr_pipe) or C(openssl req ...)."
- "The CSR may contain multiple Subject Alternate Names, but each one
will lead to an individual challenge that must be fulfilled for the
CSR to be signed."
- "I(Note): the private key used to create the CSR I(must not) be the
account key. This is a bad idea from a security point of view, and
the CA should not accept the CSR. The ACME server should return an
error in this case."
- Precisely one of O(csr) or O(csr_content) must be specified.
type: str
version_added: 1.2.0
data:
description:
@@ -292,6 +272,32 @@ options:
- "The identifier must be of the form
V(C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10)."
type: str
include_renewal_cert_id:
description:
- Determines whether to request renewal of an existing certificate according to
L(the ACME ARI draft 3, https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-5).
- This is only used when the certificate specified in O(dest) or O(fullchain_dest) already exists.
- V(never) never sends the certificate ID of the certificate to renew. V(always) will always send it.
- V(when_ari_supported) only sends the certificate ID if the ARI endpoint is found in the ACME directory.
- Generally you should use V(when_ari_supported) if you know that the ACME service supports a compatible
draft (or final version, once it is out) of the ARI extension. V(always) should never be necessary.
If you are not sure, or if you receive strange errors on invalid C(replaces) values in order objects,
use V(never), which also happens to be the default.
- ACME servers might refuse to create new orders with C(replaces) for certificates that already have an
existing order. This can happen if this module is used to create an order, and then the playbook/role
fails in case the challenges cannot be set up. If the playbook/role does not record the order data to
continue with the existing order, but tries to create a new one on the next run, creating the new order
might fail. For this reason, this option should only be set to a value different from V(never) if the
role/playbook using it keeps track of order data accross restarts, or if it takes care to deactivate
orders whose processing is aborted. Orders can be deactivated with the
M(community.crypto.acme_certificate_deactivate_authz) module.
type: str
choices:
- never
- when_ari_supported
- always
default: never
version_added: 2.20.0
'''
EXAMPLES = r'''
@@ -305,9 +311,10 @@ EXAMPLES = r'''
register: sample_com_challenge
# Alternative first step:
- name: Create a challenge for sample.com using a account key from hashi vault.
- name: Create a challenge for sample.com using a account key from Hashi Vault.
community.crypto.acme_certificate:
account_key_content: "{{ lookup('hashi_vault', 'secret=secret/account_private_key:value') }}"
account_key_content: >-
{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/account_private_key:value') }}
csr: /etc/pki/cert/csr/sample.com.csr
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
register: sample_com_challenge
@@ -374,7 +381,7 @@ EXAMPLES = r'''
# state: present
# wait: true
# # Note: route53 requires TXT entries to be enclosed in quotes
# value: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].resource_value | regex_replace('^(.*)$', '\"\\1\"') }}"
# value: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].resource_value | community.dns.quote_txt(always_quote=true) }}"
# when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge.challenge_data
#
# Alternative way:
@@ -389,7 +396,7 @@ EXAMPLES = r'''
# wait: true
# # Note: item.value is a list of TXT entries, and route53
# # requires every entry to be enclosed in quotes
# value: "{{ item.value | map('regex_replace', '^(.*)$', '\"\\1\"' ) | list }}"
# value: "{{ item.value | map('community.dns.quote_txt', always_quote=true) | list }}"
# loop: "{{ sample_com_challenge.challenge_data_dns | dict2items }}"
# when: sample_com_challenge is changed
@@ -445,39 +452,55 @@ challenge_data:
- Per identifier / challenge type challenge data.
- Since Ansible 2.8.5, only challenges which are not yet valid are returned.
returned: changed
type: list
elements: dict
type: dict
contains:
resource:
description: The challenge resource that must be created for validation.
returned: changed
type: str
sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA
resource_original:
identifier:
description:
- The original challenge resource including type identifier for V(tls-alpn-01)
challenges.
returned: changed and O(challenge) is V(tls-alpn-01)
type: str
sample: DNS:example.com
resource_value:
description:
- The value the resource has to produce for the validation.
- For V(http-01) and V(dns-01) challenges, the value can be used as-is.
- "For V(tls-alpn-01) challenges, note that this return value contains a
Base64 encoded version of the correct binary blob which has to be put
into the acmeValidation x509 extension; see
U(https://www.rfc-editor.org/rfc/rfc8737.html#section-3)
for details. To do this, you might need the P(ansible.builtin.b64decode#filter) Jinja filter
to extract the binary blob from this return value."
- For every identifier, provides a dictionary of challenge types mapping to challenge data.
- The keys in this dictionary are the identifiers. C(identifier) is a placeholder used in the documentation.
- Note that the keys are not valid Jinja2 identifiers.
returned: changed
type: str
sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
record:
description: The full DNS record's name for the challenge.
returned: changed and challenge is V(dns-01)
type: str
sample: _acme-challenge.example.com
type: dict
contains:
challenge-type:
description:
- Data for every challenge type.
- The keys in this dictionary are the challenge types. C(challenge-type) is a placeholder used in the documentation.
Possible keys are V(http-01), V(dns-01), and V(tls-alpn-01).
- Note that the keys are not valid Jinja2 identifiers.
returned: changed
type: dict
contains:
resource:
description: The challenge resource that must be created for validation.
returned: changed
type: str
sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA
resource_original:
description:
- The original challenge resource including type identifier for V(tls-alpn-01)
challenges.
returned: changed and O(challenge) is V(tls-alpn-01)
type: str
sample: DNS:example.com
resource_value:
description:
- The value the resource has to produce for the validation.
- For V(http-01) and V(dns-01) challenges, the value can be used as-is.
- "For V(tls-alpn-01) challenges, note that this return value contains a
Base64 encoded version of the correct binary blob which has to be put
into the acmeValidation x509 extension; see
U(https://www.rfc-editor.org/rfc/rfc8737.html#section-3)
for details. To do this, you might need the P(ansible.builtin.b64decode#filter) Jinja filter
to extract the binary blob from this return value."
returned: changed
type: str
sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
record:
description: The full DNS record's name for the challenge.
returned: changed and challenge is V(dns-01)
type: str
sample: _acme-challenge.example.com
challenge_data_dns:
description:
- List of TXT values per DNS record, in case challenge is V(dns-01).
@@ -546,11 +569,9 @@ all_chains:
import os
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
get_default_argspec,
create_default_argspec,
ACMEClient,
)
@@ -584,6 +605,7 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.orders impor
)
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
compute_cert_id,
pem_to_der,
)
@@ -620,6 +642,7 @@ class ACMECertificateClient(object):
self.order_uri = self.data.get('order_uri') if self.data else None
self.all_chains = None
self.select_chain_matcher = []
self.include_renewal_cert_id = module.params['include_renewal_cert_id']
if self.module.params['select_chain']:
for criterium_idx, criterium in enumerate(self.module.params['select_chain']):
@@ -660,7 +683,7 @@ class ACMECertificateClient(object):
raise ModuleFailException("CSR %s not found" % (self.csr))
# Extract list of identifiers from CSR
self.identifiers = self.client.backend.get_csr_identifiers(csr_filename=self.csr, csr_content=self.csr_content)
self.identifiers = self.client.backend.get_ordered_csr_identifiers(csr_filename=self.csr, csr_content=self.csr_content)
def is_first_step(self):
'''
@@ -677,6 +700,15 @@ class ACMECertificateClient(object):
# stored in self.order_uri by the constructor).
return self.order_uri is None
def _get_cert_info_or_none(self):
if self.module.params.get('dest'):
filename = self.module.params['dest']
else:
filename = self.module.params['fullchain_dest']
if not os.path.exists(filename):
return None
return self.client.backend.get_cert_information(cert_filename=filename)
def start_challenges(self):
'''
Create new authorizations for all identifiers of the CSR,
@@ -691,7 +723,19 @@ class ACMECertificateClient(object):
authz = Authorization.create(self.client, identifier_type, identifier)
self.authorizations[authz.combined_identifier] = authz
else:
self.order = Order.create(self.client, self.identifiers)
replaces_cert_id = None
if (
self.include_renewal_cert_id == 'always' or
(self.include_renewal_cert_id == 'when_ari_supported' and self.client.directory.has_renewal_info_endpoint())
):
cert_info = self._get_cert_info_or_none()
if cert_info is not None:
replaces_cert_id = compute_cert_id(
self.client.backend,
cert_info=cert_info,
none_if_required_information_is_missing=True,
)
self.order = Order.create(self.client, self.identifiers, replaces_cert_id)
self.order_uri = self.order.url
self.order.load_authorizations(self.client)
self.authorizations.update(self.order.authorizations)
@@ -853,15 +897,14 @@ class ACMECertificateClient(object):
def main():
argument_spec = get_default_argspec()
argument_spec.update(dict(
argument_spec = create_default_argspec(with_certificate=True)
argument_spec.argument_spec['csr']['aliases'] = ['src']
argument_spec.update_argspec(
modify_account=dict(type='bool', default=True),
account_email=dict(type='str'),
agreement=dict(type='str'),
terms_agreed=dict(type='bool', default=False),
challenge=dict(type='str', default='http-01', choices=['http-01', 'dns-01', 'tls-alpn-01', NO_CHALLENGE]),
csr=dict(type='path', aliases=['src']),
csr_content=dict(type='str'),
data=dict(type='dict'),
dest=dict(type='path', aliases=['cert']),
fullchain_dest=dict(type='path', aliases=['fullchain']),
@@ -877,20 +920,14 @@ def main():
subject_key_identifier=dict(type='str'),
authority_key_identifier=dict(type='str'),
)),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_one_of=(
['account_key_src', 'account_key_content'],
['dest', 'fullchain_dest'],
['csr', 'csr_content'],
),
mutually_exclusive=(
['account_key_src', 'account_key_content'],
['csr', 'csr_content'],
),
supports_check_mode=True,
include_renewal_cert_id=dict(type='str', choices=['never', 'when_ari_supported', 'always'], default='never'),
)
argument_spec.update(
required_one_of=[
['dest', 'fullchain_dest'],
],
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, False)
try:

View File

@@ -0,0 +1,119 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: acme_certificate_deactivate_authz
author: "Felix Fontein (@felixfontein)"
version_added: 2.20.0
short_description: Deactivate all authz for an ACME v2 order
description:
- "Deactivate all authentication objects (authz) for an ACME v2 order,
which effectively deactivates (invalidates) the order itself."
- "Authentication objects are bound to an account key and remain valid
for a certain amount of time, and can be used to issue certificates
without having to re-authenticate the domain. This can be a security
concern."
- "Another reason to use this module is to deactivate an order whose
processing failed when using O(community.crypto.acme_certificate#module:include_renewal_cert_id)."
seealso:
- module: community.crypto.acme_certificate
extends_documentation_fragment:
- community.crypto.acme.basic
- community.crypto.acme.account
- community.crypto.attributes
- community.crypto.attributes.actiongroup_acme
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
order_uri:
description:
- The ACME v2 order to deactivate.
- Can be obtained from RV(community.crypto.acme_certificate#module:order_uri).
type: str
required: true
'''
EXAMPLES = r'''
- name: Deactivate all authzs for an order
community.crypto.acme_certificate_deactivate_authz:
account_key_content: "{{ account_private_key }}"
order_uri: "{{ certificate_result.order_uri }}"
'''
RETURN = '''#'''
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
create_default_argspec,
ACMEClient,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
ACMEAccount,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ModuleFailException,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.orders import (
Order,
)
def main():
argument_spec = create_default_argspec()
argument_spec.update_argspec(
order_uri=dict(type='str', required=True),
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
if module.params['acme_version'] == 1:
module.fail_json('The module does not support acme_version=1')
backend = create_backend(module, False)
try:
client = ACMEClient(module, backend)
account = ACMEAccount(client)
dummy, account_data = account.setup_account(allow_creation=False)
if account_data is None:
raise ModuleFailException(msg='Account does not exist or is deactivated.')
order = Order.from_url(client, module.params['order_uri'])
order.load_authorizations(client)
changed = False
for authz in order.authorizations.values():
if not authz.can_deactivate():
continue
changed = True
if module.check_mode:
continue
try:
authz.deactivate(client)
except Exception:
# ignore errors
pass
if authz.status != 'deactivated':
module.warn(warning='Could not deactivate authz object {0}.'.format(authz.url))
module.exit_json(changed=changed)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,245 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2018 Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: acme_certificate_renewal_info
author: "Felix Fontein (@felixfontein)"
version_added: 2.20.0
short_description: Determine whether a certificate should be renewed or not
description:
- Uses various information to determine whether a certificate should be renewed or not.
- If available, the ARI extension (ACME Renewal Information, U(https://datatracker.ietf.org/doc/draft-ietf-acme-ari/))
is used. This module implements version 3 of the ARI draft."
extends_documentation_fragment:
- community.crypto.acme.basic
- community.crypto.acme.no_account
- community.crypto.attributes
- community.crypto.attributes.info_module
options:
certificate_path:
description:
- A path to the X.509 certificate to determine renewal of.
- In case the certificate does not exist, the module will always return RV(should_renew=true).
- O(certificate_path) and O(certificate_content) are mutually exclusive.
type: path
certificate_content:
description:
- The content of the X.509 certificate to determine renewal of.
- O(certificate_path) and O(certificate_content) are mutually exclusive.
type: str
use_ari:
description:
- Whether to use ARI information, if available.
- Set this to V(false) if the ACME server implements ARI in a way that is incompatible with this module.
type: bool
default: true
ari_algorithm:
description:
- If ARI information is used, selects which algorithm is used to determine whether to renew now.
- V(standard) selects the L(algorithm provided in the the ARI specification,
https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#name-renewalinfo-objects).
- V(start) returns RV(should_renew=true) once the start of the renewal interval has been reached.
type: str
choices:
- standard
- start
default: standard
remaining_days:
description:
- The number of days the certificate must have left being valid.
- For example, if O(remaining_days=20), this check causes RV(should_renew=true) if the
certificate is valid for less than 20 days.
type: int
remaining_percentage:
description:
- The percentage of the certificate's validity period that should be left.
- For example, if O(remaining_percentage=0.1), and the certificate's validity period is 90 days,
this check causes RV(should_renew=true) if the certificate is valid for less than 9 days.
- Must be a value between 0 and 1.
type: float
now:
description:
- Use this timestamp instead of the current timestamp to determine whether a certificate should be renewed.
- Time can be specified either as relative time or as absolute timestamp.
- Time will always be interpreted as UTC.
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ C([w | d | h | m | s]) (for example V(+32w1d2h)).
type: str
seealso:
- module: community.crypto.acme_certificate
description: Allows to obtain a certificate using the ACME protocol
- module: community.crypto.acme_ari_info
description: Obtain renewal information for a certificate
'''
EXAMPLES = '''
- name: Retrieve renewal information for a certificate
community.crypto.acme_certificate_renewal_info:
certificate_path: /etc/httpd/ssl/sample.com.crt
register: cert_data
- name: Should the certificate be renewed?
ansible.builtin.debug:
var: cert_data.should_renew
'''
RETURN = '''
should_renew:
description:
- Whether the certificate should be renewed.
- If no certificate is provided, or the certificate is expired, will always be V(true).
returned: success
type: bool
sample: true
msg:
description:
- Information on the reason for renewal.
- Should be shown to the user, as in case of ARI triggered renewal it can contain important
information, for example on forced revocations for misissued certificates.
type: str
returned: success
sample: The certificate does not exist.
supports_ari:
description:
- Whether ARI information was used to determine renewal. This can be used to determine whether to
specify O(community.crypto.acme_certificate#module:include_renewal_cert_id=when_ari_supported)
for the M(community.crypto.acme_certificate) module.
- If O(use_ari=false), this will always be V(false).
returned: success
type: bool
sample: true
cert_id:
description:
- The certificate ID according to the L(ARI specification, https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-4.1).
returned: success, the certificate exists, and has an Authority Key Identifier X.509 extension
type: str
sample: aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE
'''
import os
import random
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
create_default_argspec,
ACMEClient,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import compute_cert_id
def main():
argument_spec = create_default_argspec(with_account=False)
argument_spec.update_argspec(
certificate_path=dict(type='path'),
certificate_content=dict(type='str'),
use_ari=dict(type='bool', default=True),
ari_algorithm=dict(type='str', choices=['standard', 'start'], default='standard'),
remaining_days=dict(type='int'),
remaining_percentage=dict(type='float'),
now=dict(type='str'),
)
argument_spec.update(
mutually_exclusive=(
['certificate_path', 'certificate_content'],
),
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True)
result = dict(
changed=False,
msg='The certificate is still valid and no condition was reached',
supports_ari=False,
)
def complete(should_renew, **kwargs):
result['should_renew'] = should_renew
result.update(kwargs)
module.exit_json(**result)
if not module.params['certificate_path'] and not module.params['certificate_content']:
complete(True, msg='No certificate was specified')
if module.params['certificate_path'] is not None and not os.path.exists(module.params['certificate_path']):
complete(True, msg='The certificate file does not exist')
try:
cert_info = backend.get_cert_information(
cert_filename=module.params['certificate_path'],
cert_content=module.params['certificate_content'],
)
cert_id = compute_cert_id(backend, cert_info=cert_info, none_if_required_information_is_missing=True)
if cert_id is not None:
result['cert_id'] = cert_id
if module.params['now']:
now = backend.parse_module_parameter(module.params['now'], 'now')
else:
now = backend.get_now()
if now >= cert_info.not_valid_after:
complete(True, msg='The certificate has already expired')
client = ACMEClient(module, backend)
if cert_id is not None and module.params['use_ari'] and client.directory.has_renewal_info_endpoint():
renewal_info = client.get_renewal_info(cert_id=cert_id)
window_start = backend.parse_acme_timestamp(renewal_info['suggestedWindow']['start'])
window_end = backend.parse_acme_timestamp(renewal_info['suggestedWindow']['end'])
msg_append = ''
if 'explanationURL' in renewal_info:
msg_append = '. Information on renewal interval: {0}'.format(renewal_info['explanationURL'])
result['supports_ari'] = True
if now > window_end:
complete(True, msg='The suggested renewal interval provided by ARI is in the past{0}'.format(msg_append))
if module.params['ari_algorithm'] == 'start':
if now > window_start:
complete(True, msg='The suggested renewal interval provided by ARI has begun{0}'.format(msg_append))
else:
random_time = backend.interpolate_timestamp(window_start, window_end, random.random())
if now > random_time:
complete(
True,
msg='The picked random renewal time {0} in sugested renewal internal provided by ARI is in the past{1}'.format(
random_time,
msg_append,
),
)
if module.params['remaining_days'] is not None:
remaining_days = (cert_info.not_valid_after - now).days
if remaining_days < module.params['remaining_days']:
complete(True, msg='The certificate expires in {0} days'.format(remaining_days))
if module.params['remaining_percentage'] is not None:
timestamp = backend.interpolate_timestamp(cert_info.not_valid_before, cert_info.not_valid_after, 1 - module.params['remaining_percentage'])
if timestamp < now:
complete(
True,
msg="The remaining percentage {0}% of the certificate's lifespan was reached on {1}".format(
module.params['remaining_percentage'] * 100,
timestamp,
),
)
complete(False)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
main()

View File

@@ -37,7 +37,8 @@ seealso:
- module: community.crypto.acme_inspect
description: Allows to debug problems.
extends_documentation_fragment:
- community.crypto.acme
- community.crypto.acme.basic
- community.crypto.acme.account
- community.crypto.attributes
- community.crypto.attributes.actiongroup_acme
attributes:
@@ -127,11 +128,9 @@ EXAMPLES = '''
RETURN = '''#'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
get_default_argspec,
create_default_argspec,
ACMEClient,
)
@@ -152,24 +151,23 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.utils import
def main():
argument_spec = get_default_argspec()
argument_spec.update(dict(
argument_spec = create_default_argspec(require_account_key=False)
argument_spec.update_argspec(
private_key_src=dict(type='path'),
private_key_content=dict(type='str', no_log=True),
private_key_passphrase=dict(type='str', no_log=True),
certificate=dict(type='path', required=True),
revoke_reason=dict(type='int'),
))
module = AnsibleModule(
argument_spec=argument_spec,
)
argument_spec.update(
required_one_of=(
['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'],
),
mutually_exclusive=(
['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'],
),
supports_check_mode=False,
)
module = argument_spec.create_ansible_module()
backend = create_backend(module, False)
try:

View File

@@ -165,6 +165,16 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.io import (
read_file,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
set_not_valid_after,
set_not_valid_before,
)
from ansible_collections.community.crypto.plugins.module_utils.time import (
get_now_datetime,
)
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
@@ -244,8 +254,9 @@ def main():
domain = to_text(challenge_data['resource'])
identifier_type, identifier = to_text(challenge_data.get('resource_original', 'dns:' + challenge_data['resource'])).split(':', 1)
subject = issuer = cryptography.x509.Name([])
not_valid_before = datetime.datetime.utcnow()
not_valid_after = datetime.datetime.utcnow() + datetime.timedelta(days=10)
now = get_now_datetime(with_timezone=CRYPTOGRAPHY_TIMEZONE)
not_valid_before = now
not_valid_after = now + datetime.timedelta(days=10)
if identifier_type == 'dns':
san = cryptography.x509.DNSName(identifier)
elif identifier_type == 'ip':
@@ -254,7 +265,7 @@ def main():
raise ModuleFailException('Unsupported identifier type "{0}"'.format(identifier_type))
# Generate regular self-signed certificate
regular_certificate = cryptography.x509.CertificateBuilder().subject_name(
cert_builder = cryptography.x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
@@ -262,14 +273,13 @@ def main():
private_key.public_key()
).serial_number(
cryptography.x509.random_serial_number()
).not_valid_before(
not_valid_before
).not_valid_after(
not_valid_after
).add_extension(
cryptography.x509.SubjectAlternativeName([san]),
critical=False,
).sign(
)
cert_builder = set_not_valid_before(cert_builder, not_valid_before)
cert_builder = set_not_valid_after(cert_builder, not_valid_after)
regular_certificate = cert_builder.sign(
private_key,
cryptography.hazmat.primitives.hashes.SHA256(),
_cryptography_backend
@@ -278,7 +288,7 @@ def main():
# Process challenge
if challenge == 'tls-alpn-01':
value = base64.b64decode(challenge_data['resource_value'])
challenge_certificate = cryptography.x509.CertificateBuilder().subject_name(
cert_builder = cryptography.x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
@@ -286,10 +296,6 @@ def main():
private_key.public_key()
).serial_number(
cryptography.x509.random_serial_number()
).not_valid_before(
not_valid_before
).not_valid_after(
not_valid_after
).add_extension(
cryptography.x509.SubjectAlternativeName([san]),
critical=False,
@@ -299,7 +305,10 @@ def main():
encode_octet_string(value),
),
critical=True,
).sign(
)
cert_builder = set_not_valid_before(cert_builder, not_valid_before)
cert_builder = set_not_valid_after(cert_builder, not_valid_after)
challenge_certificate = cert_builder.sign(
private_key,
cryptography.hazmat.primitives.hashes.SHA256(),
_cryptography_backend

View File

@@ -42,7 +42,8 @@ seealso:
description: The specification of the C(tls-alpn-01) challenge (RFC 8737).
link: https://www.rfc-editor.org/rfc/rfc8737.html
extends_documentation_fragment:
- community.crypto.acme
- community.crypto.acme.basic
- community.crypto.acme.account
- community.crypto.attributes
- community.crypto.attributes.actiongroup_acme
attributes:
@@ -247,12 +248,11 @@ output_json:
- ...
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
get_default_argspec,
create_default_argspec,
ACMEClient,
)
@@ -263,18 +263,14 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
def main():
argument_spec = get_default_argspec()
argument_spec.update(dict(
argument_spec = create_default_argspec(require_account_key=False)
argument_spec.update_argspec(
url=dict(type='str'),
method=dict(type='str', choices=['get', 'post', 'directory-only'], default='get'),
content=dict(type='str'),
fail_on_acme_error=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
mutually_exclusive=(
['account_key_src', 'account_key_content'],
),
)
argument_spec.update(
required_if=(
['method', 'get', ['url']],
['method', 'post', ['url', 'content']],
@@ -282,6 +278,7 @@ def main():
['method', 'post', ['account_key_src', 'account_key_content'], True],
),
)
module = argument_spec.create_ansible_module()
backend = create_backend(module, False)
result = dict()

View File

@@ -78,7 +78,7 @@ EXAMPLES = '''
# certificates, finds the associated root certificate.
- name: Find root certificate
community.crypto.certificate_complete_chain:
input_chain: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com-fullchain.pem') }}"
input_chain: "{{ lookup('ansible.builtin.file', '/etc/ssl/csr/www.ansible.com-fullchain.pem') }}"
root_certificates:
- /etc/ca-certificates/
register: www_ansible_com
@@ -91,7 +91,7 @@ EXAMPLES = '''
# certificates, finds the associated root certificate.
- name: Find root certificate
community.crypto.certificate_complete_chain:
input_chain: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.pem') }}"
input_chain: "{{ lookup('ansible.builtin.file', '/etc/ssl/csr/www.ansible.com.pem') }}"
intermediate_certificates:
- /etc/ssl/csr/www.ansible.com-chain.pem
root_certificates:
@@ -142,6 +142,11 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import
split_pem_list,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
CRYPTOGRAPHY_HAS_ED448_SIGN,
CRYPTOGRAPHY_HAS_ED25519_SIGN,
)
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
@@ -196,6 +201,12 @@ def is_parent(module, cert, potential_parent):
cert.cert.tbs_certificate_bytes,
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cert.cert.signature_hash_algorithm),
)
elif CRYPTOGRAPHY_HAS_ED25519_SIGN and isinstance(
public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey):
public_key.verify(cert.cert.signature, cert.cert.tbs_certificate_bytes)
elif CRYPTOGRAPHY_HAS_ED448_SIGN and isinstance(
public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey):
public_key.verify(cert.cert.signature, cert.cert.tbs_certificate_bytes)
else:
# Unknown public key type
module.warn('Unknown public key type "{0}"'.format(public_key))

View File

@@ -96,7 +96,7 @@ options:
obtained using O(request_type).
- If O(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
O(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 O(remaining_days) to a value V(60) or higher).
- For example, if you are requesting Certificates with a 90 day lifetime, do not set O(remaining_days) to a value V(60) or higher).
- The O(force) option may be used to ensure that a new certificate is always obtained.
type: int
default: 30
@@ -350,6 +350,8 @@ seealso:
description: Can be used to create private keys (both for certificates and accounts).
- module: community.crypto.openssl_csr
description: Can be used to create a Certificate Signing Request (CSR).
- plugin: community.crypto.to_serial
plugin_type: filter
'''
EXAMPLES = r'''
@@ -490,7 +492,10 @@ tracking_id:
type: int
sample: 380079
serial_number:
description: The serial number of the issued certificate.
description:
- The serial number of the issued certificate.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
returned: success
type: int
sample: 1235262234164342
@@ -933,8 +938,8 @@ def main():
module.fail_json(msg='The cert_expiry field is invalid when request_type="reissue".')
elif module.params['cert_lifetime']:
module.fail_json(msg='The cert_lifetime field is invalid when request_type="reissue".')
# Only a reissued request can omit the CSR
else:
# Reissued or renew request can omit the CSR
elif module.params['request_type'] != 'renew':
module_params_csr = module.params['csr']
if module_params_csr is None:
module.fail_json(msg='The csr field is required when request_type={0}'.format(module.params['request_type']))

View File

@@ -15,165 +15,211 @@ module: get_certificate
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 uses the cryptography Python library.
- Support SNI (L(Server Name Indication,https://en.wikipedia.org/wiki/Server_Name_Indication)) only with python >= 2.7.
- Makes a secure connection and returns information about the presented certificate.
- 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 and newer.
extends_documentation_fragment:
- community.crypto.attributes
- community.crypto.attributes
attributes:
check_mode:
support: none
details:
- This action does not modify state.
diff_mode:
support: N/A
details:
- This action does not modify state.
check_mode:
support: none
details:
- This action does not modify state.
diff_mode:
support: N/A
details:
- This action does not modify state.
options:
host:
description:
- The host to get the cert for (IP is fine)
type: str
required: true
ca_cert:
description:
- A PEM file containing one or more root certificates; if present, the cert will be validated against these root certs.
- Note that this only validates the certificate is signed by the chain; not that the cert is valid for the host presenting it.
type: path
port:
description:
- The port to connect to
type: int
required: true
server_name:
description:
- Server name used for SNI (L(Server Name Indication,https://en.wikipedia.org/wiki/Server_Name_Indication)) when hostname
is an IP or is different from server name.
type: str
version_added: 1.4.0
proxy_host:
description:
- Proxy host used when get a certificate.
type: str
proxy_port:
description:
- Proxy port used when get a certificate.
type: int
default: 8080
starttls:
description:
- Requests a secure connection for protocols which require clients to initiate encryption.
- Only available for V(mysql) currently.
type: str
choices:
- mysql
version_added: 1.9.0
timeout:
description:
- The timeout in seconds
type: int
default: 10
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is V(auto), which tries to use C(cryptography) if available.
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [ auto, cryptography ]
ciphers:
description:
- SSL/TLS Ciphers to use for the request.
- 'When a list is provided, all ciphers are joined in order with V(:).'
- See the L(OpenSSL Cipher List Format,https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html#CIPHER-LIST-FORMAT)
for more details.
- The available ciphers is dependent on the Python and OpenSSL/LibreSSL versions.
type: list
elements: str
version_added: 2.11.0
asn1_base64:
description:
- Whether to encode the ASN.1 values in the RV(extensions) return value with Base64 or not.
- The documentation claimed for a long time that the values are Base64 encoded, but they
never were. For compatibility this option is set to V(false).
- The default value V(false) is B(deprecated) and will change to V(true) in community.crypto 3.0.0.
type: bool
version_added: 2.12.0
host:
description:
- The host to get the cert for (IP is fine).
type: str
required: true
ca_cert:
description:
- A PEM file containing one or more root certificates; if present, the cert will be validated against these root certs.
- Note that this only validates the certificate is signed by the chain; not that the cert is valid for the host presenting it.
type: path
port:
description:
- The port to connect to.
type: int
required: true
server_name:
description:
- Server name used for SNI (L(Server Name Indication,https://en.wikipedia.org/wiki/Server_Name_Indication)) when hostname
is an IP or is different from server name.
type: str
version_added: 1.4.0
proxy_host:
description:
- Proxy host used when get a certificate.
type: str
proxy_port:
description:
- Proxy port used when get a certificate.
type: int
default: 8080
starttls:
description:
- Requests a secure connection for protocols which require clients to initiate encryption.
- Only available for V(mysql) currently.
type: str
choices:
- mysql
version_added: 1.9.0
timeout:
description:
- The timeout in seconds.
type: int
default: 10
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is V(auto), which tries to use C(cryptography) if available.
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [ auto, cryptography ]
ciphers:
description:
- SSL/TLS Ciphers to use for the request.
- 'When a list is provided, all ciphers are joined in order with V(:).'
- See the L(OpenSSL Cipher List Format,https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html#CIPHER-LIST-FORMAT)
for more details.
- The available ciphers is dependent on the Python and OpenSSL/LibreSSL versions.
type: list
elements: str
version_added: 2.11.0
asn1_base64:
description:
- Whether to encode the ASN.1 values in the RV(extensions) return value with Base64 or not.
- The documentation claimed for a long time that the values are Base64 encoded, but they
never were. For compatibility this option is set to V(false).
- The default value V(false) is B(deprecated) and will change to V(true) in community.crypto 3.0.0.
type: bool
version_added: 2.12.0
tls_ctx_options:
description:
- TLS context options (TLS/SSL OP flags) to use for the request.
- See the L(List of SSL OP Flags,https://wiki.openssl.org/index.php/List_of_SSL_OP_Flags) for more details.
- The available TLS context options is dependent on the Python and OpenSSL/LibreSSL versions.
type: list
elements: raw
version_added: 2.21.0
get_certificate_chain:
description:
- If set to V(true), will obtain the certificate chain next to the certificate itself.
- The chain as returned by the server can be found in RV(unverified_chain), and the chain that passed validation
in RV(verified_chain).
- B(Note) that this needs B(Python 3.10 or newer). Also note that only Python 3.13 or newer officially supports this.
The module uses internal APIs of Python 3.10, 3.11, and 3.12 to achieve the same. It can be that future versions of
Python 3.10, 3.11, or 3.12 break this.
type: bool
default: false
version_added: 2.21.0
notes:
- When using ca_cert on OS X it has been reported that in some conditions the validate will always succeed.
- 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 O(proxy_host)"
- "cryptography >= 1.6"
- "Python >= 2.7 when using O(proxy_host), and Python >= 3.10 when O(get_certificate_chain=true)"
- "cryptography >= 1.6"
seealso:
- plugin: community.crypto.to_serial
plugin_type: filter
'''
RETURN = '''
cert:
description: The certificate retrieved from the port
returned: success
type: str
description: The certificate retrieved from the port.
returned: success
type: str
expired:
description: Boolean indicating if the cert is expired
returned: success
type: bool
description: Boolean indicating if the cert is expired.
returned: success
type: bool
extensions:
description: Extensions applied to the cert
returned: success
type: list
elements: dict
contains:
critical:
returned: success
type: bool
description: Whether the extension is critical.
asn1_data:
returned: success
type: str
description:
- The ASN.1 content of the extension.
- If O(asn1_base64=true) this will be Base64 encoded, otherwise the raw
binary value will be returned.
- Please note that the raw binary value might not survive JSON serialization
to the Ansible controller, and also might cause failures when displaying it.
See U(https://github.com/ansible/ansible/issues/80258) for more information.
- 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
description: The extension's name.
description: Extensions applied to the cert.
returned: success
type: list
elements: dict
contains:
critical:
returned: success
type: bool
description: Whether the extension is critical.
asn1_data:
returned: success
type: str
description:
- The ASN.1 content of the extension.
- If O(asn1_base64=true) this will be Base64 encoded, otherwise the raw
binary value will be returned.
- Please note that the raw binary value might not survive JSON serialization
to the Ansible controller, and also might cause failures when displaying it.
See U(https://github.com/ansible/ansible/issues/80258) for more information.
- 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
description: The extension's name.
issuer:
description: Information about the issuer of the cert
returned: success
type: dict
description: Information about the issuer of the cert.
returned: success
type: dict
not_after:
description: Expiration date of the cert
returned: success
type: str
description: Expiration date of the cert.
returned: success
type: str
not_before:
description: Issue date of the cert
returned: success
type: str
description: Issue date of the cert.
returned: success
type: str
serial_number:
description: The serial number of the cert
returned: success
type: str
description:
- The serial number of the cert.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
returned: success
type: int
signature_algorithm:
description: The algorithm used to sign the cert
returned: success
type: str
description: The algorithm used to sign the cert.
returned: success
type: str
subject:
description: Information about the subject of the cert (OU, CN, etc)
returned: success
type: dict
description: Information about the subject of the cert (C(OU), C(CN), and so on).
returned: success
type: dict
version:
description: The version number of the certificate
returned: success
type: str
description: The version number of the certificate.
returned: success
type: str
verified_chain:
description:
- The verified certificate chain retrieved from the port.
- The first entry is always RV(cert).
- The last certificate the root certificate the chain is traced to. If O(ca_cert) is provided this certificate is part of that store;
otherwise it is part of the store used by default by Python.
- Note that RV(unverified_chain) generally does not contain the root certificate, and might contain other certificates that are not part
of the validated chain.
returned: success and O(get_certificate_chain=true)
type: list
elements: str
version_added: 2.21.0
unverified_chain:
description:
- The certificate chain retrieved from the port.
- The first entry is always RV(cert).
returned: success and O(get_certificate_chain=true)
type: list
elements: str
version_added: 2.21.0
'''
EXAMPLES = '''
@@ -197,26 +243,56 @@ EXAMPLES = '''
ansible.builtin.debug:
msg: "cert expires in: {{ expire_days }} days."
vars:
expire_days: "{{ (( cert.not_after | to_datetime('%Y%m%d%H%M%SZ')) - (ansible_date_time.iso8601 | to_datetime('%Y-%m-%dT%H:%M:%SZ')) ).days }}"
expire_days: >-
{{ (
(cert.not_after | ansible.builtin.to_datetime('%Y%m%d%H%M%SZ')) -
(ansible_date_time.iso8601 | ansible.builtin.to_datetime('%Y-%m-%dT%H:%M:%SZ'))
).days }}
- name: Allow legacy insecure renegotiation to get a cert from a legacy device
community.crypto.get_certificate:
host: "legacy-device.domain.com"
port: 443
ciphers:
- HIGH
tls_ctx_options:
- OP_ALL
- OP_NO_SSLv3
- OP_CIPHER_SERVER_PREFERENCE
- OP_ENABLE_MIDDLEBOX_COMPAT
- OP_NO_COMPRESSION
- 4 # OP_LEGACY_SERVER_CONNECT
delegate_to: localhost
run_once: true
register: legacy_cert
'''
import atexit
import base64
import datetime
import traceback
import ssl
import sys
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
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_oid_to_name,
cryptography_get_extensions_from_cert,
get_not_valid_after,
get_not_valid_before,
)
from ansible_collections.community.crypto.plugins.module_utils.time import (
get_now_datetime,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
@@ -272,6 +348,8 @@ def main():
starttls=dict(type='str', choices=['mysql']),
ciphers=dict(type='list', elements='str'),
asn1_base64=dict(type='bool'),
tls_ctx_options=dict(type='list', elements='raw'),
get_certificate_chain=dict(type='bool', default=False),
),
)
@@ -285,6 +363,9 @@ def main():
start_tls_server_type = module.params.get('starttls')
ciphers = module.params.get('ciphers')
asn1_base64 = module.params['asn1_base64']
tls_ctx_options = module.params['tls_ctx_options']
get_certificate_chain = module.params['get_certificate_chain']
if asn1_base64 is None:
module.deprecate(
'The default value `false` for asn1_base64 is deprecated and will change to `true` in '
@@ -295,6 +376,12 @@ def main():
)
asn1_base64 = False
if get_certificate_chain and sys.version_info < (3, 10):
module.fail_json(
msg='get_certificate_chain=true can only be used with Python 3.10 (Python 3.13+ officially supports this). '
'The Python version used to run the get_certificate module is %s' % sys.version
)
backend = module.params.get('select_crypto_backend')
if backend == 'auto':
# Detection what is possible
@@ -325,6 +412,9 @@ def main():
if not isfile(ca_cert):
module.fail_json(msg="ca_cert file does not exist")
verified_chain = None
unverified_chain = None
if not HAS_CREATE_DEFAULT_CONTEXT:
# Python < 2.7.9
if proxy_host:
@@ -333,6 +423,9 @@ def main():
if ciphers is not None:
module.fail_json(msg='To use ciphers, you must run the get_certificate module with Python 2.7 or newer.',
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR)
if tls_ctx_options is not None:
module.fail_json(msg='To use tls_ctx_options, you must run the get_certificate module with Python 2.7 or newer.',
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR)
try:
# Note: get_server_certificate does not support SNI!
cert = get_server_certificate((host, port), ca_certs=ca_cert)
@@ -368,8 +461,76 @@ def main():
ciphers_joined = ":".join(ciphers)
ctx.set_ciphers(ciphers_joined)
cert = ctx.wrap_socket(sock, server_hostname=server_name or host).getpeercert(True)
if tls_ctx_options is not None:
# Clear default ctx options
ctx.options = 0
# For each item in the tls_ctx_options list
for tls_ctx_option in tls_ctx_options:
# If the item is a string_type
if isinstance(tls_ctx_option, string_types):
# Convert tls_ctx_option to a native string
tls_ctx_option_str = to_native(tls_ctx_option)
# Get the tls_ctx_option_str attribute from ssl
tls_ctx_option_attr = getattr(ssl, tls_ctx_option_str, None)
# If tls_ctx_option_attr is an integer
if isinstance(tls_ctx_option_attr, int):
# Set tls_ctx_option_int to the attribute value
tls_ctx_option_int = tls_ctx_option_attr
# If tls_ctx_option_attr is not an integer
else:
module.fail_json(msg="Failed to determine the numeric value for {0}".format(tls_ctx_option_str))
# If the item is an integer
elif isinstance(tls_ctx_option, int):
# Set tls_ctx_option_int to the item value
tls_ctx_option_int = tls_ctx_option
# If the item is not a string nor integer
else:
module.fail_json(msg="tls_ctx_options must be a string or integer, got {0!r}".format(tls_ctx_option))
try:
# Add the int value of the item to ctx options
ctx.options |= tls_ctx_option_int
except Exception as e:
module.fail_json(msg="Failed to add {0} to CTX options".format(tls_ctx_option_str or tls_ctx_option_int))
tls_sock = ctx.wrap_socket(sock, server_hostname=server_name or host)
cert = tls_sock.getpeercert(True)
cert = DER_cert_to_PEM_cert(cert)
if get_certificate_chain:
if sys.version_info < (3, 13):
# The official way to access this has been added in https://github.com/python/cpython/pull/109113/files.
# We're basically doing the same for older Python versions. The internal API needed for this was added
# in https://github.com/python/cpython/commit/666991fc598bc312d72aff0078ecb553f0a968f1, which was first
# released in Python 3.10.0.
def _convert_chain(chain):
if not chain:
return []
return [c.public_bytes(ssl._ssl.ENCODING_DER) for c in chain]
ssl_obj = tls_sock._sslobj # This is of type ssl._ssl._SSLSocket
verified_der_chain = _convert_chain(ssl_obj.get_verified_chain())
unverified_der_chain = _convert_chain(ssl_obj.get_unverified_chain())
else:
# This works with Python 3.13+
# Unfortunately due to a bug (https://github.com/python/cpython/issues/118658) some early pre-releases of
# Python 3.13 do not return lists of byte strings, but lists of _ssl.Certificate objects. This is going to
# be fixed by https://github.com/python/cpython/pull/118669. For now we convert the certificates ourselves
# if they are not byte strings to work around this.
def _convert_chain(chain):
return [
c if isinstance(c, bytes) else c.public_bytes(ssl._ssl.ENCODING_DER)
for c in chain
]
verified_der_chain = _convert_chain(tls_sock.get_verified_chain())
unverified_der_chain = _convert_chain(tls_sock.get_unverified_chain())
verified_chain = [DER_cert_to_PEM_cert(c) for c in verified_der_chain]
unverified_chain = [DER_cert_to_PEM_cert(c) for c in unverified_der_chain]
except Exception as e:
if proxy_host:
module.fail_json(msg="Failed to get cert via proxy {0}:{1} from {2}:{3}, error: {4}".format(
@@ -385,7 +546,7 @@ def main():
for attribute in x509.subject:
result['subject'][cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value
result['expired'] = x509.not_valid_after < datetime.datetime.utcnow()
result['expired'] = get_not_valid_after(x509) < get_now_datetime(with_timezone=CRYPTOGRAPHY_TIMEZONE)
result['extensions'] = []
for dotted_number, entry in cryptography_get_extensions_from_cert(x509).items():
@@ -403,8 +564,8 @@ def main():
for attribute in x509.issuer:
result['issuer'][cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value
result['not_after'] = x509.not_valid_after.strftime('%Y%m%d%H%M%SZ')
result['not_before'] = x509.not_valid_before.strftime('%Y%m%d%H%M%SZ')
result['not_after'] = get_not_valid_after(x509).strftime('%Y%m%d%H%M%SZ')
result['not_before'] = get_not_valid_before(x509).strftime('%Y%m%d%H%M%SZ')
result['serial_number'] = x509.serial_number
result['signature_algorithm'] = cryptography_oid_to_name(x509.signature_algorithm_oid)
@@ -417,6 +578,11 @@ def main():
else:
result['version'] = "unknown"
if verified_chain is not None:
result['verified_chain'] = verified_chain
if unverified_chain is not None:
result['unverified_chain'] = unverified_chain
module.exit_json(**result)

View File

@@ -79,6 +79,17 @@ options:
value is a string with the passphrase."
type: str
version_added: '1.0.0'
keyslot:
description:
- "Adds the O(keyfile) or O(passphrase) to a specific keyslot when
creating a new container on O(device). Parameter value is the
number of the keyslot."
- "B(Note) that a device of O(type=luks1) supports the keyslot numbers
V(0)-V(7) and a device of O(type=luks2) supports the keyslot numbers
V(0)-V(31). In order to use the keyslots V(8)-V(31) when creating a new
container, setting O(type) to V(luks2) is required."
type: int
version_added: '2.16.0'
keysize:
description:
- "Sets the key size only if LUKS container does not exist."
@@ -108,6 +119,16 @@ options:
be used even if another keyslot already exists for this passphrase."
type: str
version_added: '1.0.0'
new_keyslot:
description:
- "Adds the additional O(new_keyfile) or O(new_passphrase) to a
specific keyslot on the given O(device). Parameter value is the number
of the keyslot."
- "B(Note) that a device of O(type=luks1) supports the keyslot numbers
V(0)-V(7) and a device of O(type=luks2) supports the keyslot numbers
V(0)-V(31)."
type: int
version_added: '2.16.0'
remove_keyfile:
description:
- "Removes given key from the container on O(device). Does not
@@ -133,6 +154,17 @@ options:
to V(true)."
type: str
version_added: '1.0.0'
remove_keyslot:
description:
- "Removes the key in the given slot on O(device). Needs
O(keyfile) or O(passphrase) for authorization."
- "B(Note) that a device of O(type=luks1) supports the keyslot numbers
V(0)-V(7) and a device of O(type=luks2) supports the keyslot numbers
V(0)-V(31)."
- "B(Note) that the given O(keyfile) or O(passphrase) must not be
in the slot to be removed."
type: int
version_added: '2.16.0'
force_remove_last_key:
description:
- "If set to V(true), allows removing the last key from a container."
@@ -261,13 +293,20 @@ options:
persistent:
description:
- "Allows the user to store options into container's metadata persistently and automatically use them next time.
Only O(perf_same_cpu_crypt), O(perf_submit_from_crypt_cpus), O(perf_no_read_workqueue), and O(perf_no_write_workqueue)
can be stored persistently."
Only O(perf_same_cpu_crypt), O(perf_submit_from_crypt_cpus), O(perf_no_read_workqueue), O(perf_no_write_workqueue),
and O(allow_discards) 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'
allow_discards:
description:
- "Allow discards (also known as TRIM) requests for device."
- "Will only be used when opening containers."
type: bool
default: false
version_added: '2.17.0'
requirements:
- "cryptsetup"
@@ -377,6 +416,26 @@ EXAMPLES = '''
state: "present"
keyfile: "/vault/keyfile"
type: luks2
- name: Create a container with key in slot 4
community.crypto.luks_device:
device: "/dev/loop0"
state: "present"
keyfile: "/vault/keyfile"
keyslot: 4
- name: Add a new key in slot 5
community.crypto.luks_device:
device: "/dev/loop0"
keyfile: "/vault/keyfile"
new_keyfile: "/vault/keyfile"
new_keyslot: 5
- name: Remove the key from slot 4 (given keyfile must not be slot 4)
community.crypto.luks_device:
device: "/dev/loop0"
keyfile: "/vault/keyfile"
remove_keyslot: 4
'''
RETURN = '''
@@ -523,6 +582,29 @@ class CryptHandler(Handler):
result = self._run_command([self._cryptsetup_bin, 'isLuks', device])
return result[RETURN_CODE] == 0
def get_luks_type(self, device):
''' get the luks type of a device
'''
if self.is_luks(device):
with open(device, 'rb') as f:
for offset in LUKS2_HEADER_OFFSETS:
f.seek(offset)
data = f.read(LUKS_HEADER_L)
if data == LUKS2_HEADER2:
return 'luks2'
return 'luks1'
return None
def is_luks_slot_set(self, device, keyslot):
''' check if a keyslot is set
'''
result = self._run_command([self._cryptsetup_bin, 'luksDump', device])
if result[RETURN_CODE] != 0:
raise ValueError('Error while dumping LUKS header from %s' % (device, ))
result_luks1 = 'Key Slot %d: ENABLED' % (keyslot) in result[STDOUT]
result_luks2 = ' %d: luks2' % (keyslot) in result[STDOUT]
return result_luks1 or result_luks2
def _add_pbkdf_options(self, options, pbkdf):
if pbkdf['iteration_time'] is not None:
options.extend(['--iter-time', str(int(pbkdf['iteration_time'] * 1000))])
@@ -535,7 +617,7 @@ class CryptHandler(Handler):
if pbkdf['parallel'] is not None:
options.extend(['--pbkdf-parallel', str(pbkdf['parallel'])])
def run_luks_create(self, device, keyfile, passphrase, keysize, cipher, hash_, sector_size, pbkdf):
def run_luks_create(self, device, keyfile, passphrase, keyslot, keysize, cipher, hash_, sector_size, pbkdf):
# create a new luks container; use batch mode to auto confirm
luks_type = self._module.params['type']
label = self._module.params['label']
@@ -556,6 +638,8 @@ class CryptHandler(Handler):
self._add_pbkdf_options(options, pbkdf)
if sector_size is not None:
options.extend(['--sector-size', str(sector_size)])
if keyslot is not None:
options.extend(['--key-slot', str(keyslot)])
args = [self._cryptsetup_bin, 'luksFormat']
args.extend(options)
@@ -569,7 +653,7 @@ class CryptHandler(Handler):
% (device, result[STDERR]))
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):
perf_no_read_workqueue, perf_no_write_workqueue, persistent, allow_discards, name):
args = [self._cryptsetup_bin]
if keyfile:
args.extend(['--key-file', keyfile])
@@ -583,6 +667,8 @@ class CryptHandler(Handler):
args.extend(['--perf-no_write_workqueue'])
if persistent:
args.extend(['--persistent'])
if allow_discards:
args.extend(['--allow-discards'])
args.extend(['open', '--type', 'luks', device, name])
result = self._run_command(args, data=passphrase)
@@ -615,7 +701,7 @@ class CryptHandler(Handler):
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):
new_passphrase, new_keyslot, pbkdf):
''' Add new key from a keyfile or passphrase to given 'device';
authentication done using 'keyfile' or 'passphrase'.
Raises ValueError when command fails.
@@ -625,6 +711,9 @@ class CryptHandler(Handler):
if pbkdf is not None:
self._add_pbkdf_options(args, pbkdf)
if new_keyslot is not None:
args.extend(['--key-slot', str(new_keyslot)])
if keyfile:
args.extend(['--key-file', keyfile])
else:
@@ -640,7 +729,7 @@ class CryptHandler(Handler):
raise ValueError('Error while adding new LUKS keyslot to %s: %s'
% (device, result[STDERR]))
def run_luks_remove_key(self, device, keyfile, passphrase,
def run_luks_remove_key(self, device, keyfile, passphrase, keyslot,
force_remove_last_key=False):
''' Remove key from given device
Raises ValueError when command fails
@@ -675,7 +764,10 @@ class CryptHandler(Handler):
"To be able to remove a key, please set "
"`force_remove_last_key` to `true`." % device)
args = [self._cryptsetup_bin, 'luksRemoveKey', device, '-q']
if keyslot is None:
args = [self._cryptsetup_bin, 'luksRemoveKey', device, '-q']
else:
args = [self._cryptsetup_bin, 'luksKillSlot', device, '-q', str(keyslot)]
if keyfile:
args.extend(['--key-file', keyfile])
result = self._run_command(args, data=passphrase)
@@ -683,7 +775,7 @@ class CryptHandler(Handler):
raise ValueError('Error while removing LUKS key from %s: %s'
% (device, result[STDERR]))
def luks_test_key(self, device, keyfile, passphrase):
def luks_test_key(self, device, keyfile, passphrase, keyslot=None):
''' Check whether the keyfile or passphrase works.
Raises ValueError when command fails.
'''
@@ -695,12 +787,22 @@ class CryptHandler(Handler):
else:
data = passphrase
if keyslot is not None:
args.extend(['--key-slot', str(keyslot)])
result = self._run_command(args, data=data)
if result[RETURN_CODE] == 0:
return True
for output in (STDOUT, STDERR):
if 'No key available with this passphrase' in result[output]:
return False
if 'No usable keyslot is available.' in result[output]:
return False
# This check is necessary due to cryptsetup in version 2.0.3 not printing 'No usable keyslot is available'
# when using the --key-slot parameter in combination with --test-passphrase
if result[RETURN_CODE] == 1 and keyslot is not None and result[STDOUT] == '' and result[STDERR] == '':
return False
raise ValueError('Error while testing whether keyslot exists on %s: %s'
% (device, result[STDERR]))
@@ -812,12 +914,20 @@ class ConditionsHandler(Handler):
self._module.fail_json(msg="Contradiction in setup: Asking to "
"add a key to absent LUKS.")
return not self._crypthandler.luks_test_key(self.device, self._module.params['new_keyfile'], self._module.params['new_passphrase'])
key_present = self._crypthandler.luks_test_key(self.device, self._module.params['new_keyfile'], self._module.params['new_passphrase'])
if self._module.params['new_keyslot'] is not None:
key_present_slot = self._crypthandler.luks_test_key(self.device, self._module.params['new_keyfile'], self._module.params['new_passphrase'],
self._module.params['new_keyslot'])
if key_present and not key_present_slot:
self._module.fail_json(msg="Trying to add key that is already present in another slot")
return not key_present
def luks_remove_key(self):
if (self.device is None or
(self._module.params['remove_keyfile'] is None and
self._module.params['remove_passphrase'] is None)):
self._module.params['remove_passphrase'] is None and
self._module.params['remove_keyslot'] is None)):
# conditions for removing a key not fulfilled
return False
@@ -825,6 +935,15 @@ class ConditionsHandler(Handler):
self._module.fail_json(msg="Contradiction in setup: Asking to "
"remove a key from absent LUKS.")
if self._module.params['remove_keyslot'] is not None:
if not self._crypthandler.is_luks_slot_set(self.device, self._module.params['remove_keyslot']):
return False
result = self._crypthandler.luks_test_key(self.device, self._module.params['keyfile'], self._module.params['passphrase'])
if self._crypthandler.luks_test_key(self.device, self._module.params['keyfile'], self._module.params['passphrase'],
self._module.params['remove_keyslot']):
self._module.fail_json(msg='Cannot remove keyslot with keyfile or passphrase in same slot.')
return result
return self._crypthandler.luks_test_key(self.device, self._module.params['remove_keyfile'], self._module.params['remove_passphrase'])
def luks_remove(self):
@@ -832,6 +951,19 @@ class ConditionsHandler(Handler):
self._module.params['state'] == 'absent' and
self._crypthandler.is_luks(self.device))
def validate_keyslot(self, param, luks_type):
if self._module.params[param] is not None:
if luks_type is None and param == 'keyslot':
if 8 <= self._module.params[param] <= 31:
self._module.fail_json(msg="You must specify type=luks2 when creating a new LUKS device to use keyslots 8-31.")
elif not (0 <= self._module.params[param] <= 7):
self._module.fail_json(msg="When not specifying a type, only the keyslots 0-7 are allowed.")
if luks_type == 'luks1' and not 0 <= self._module.params[param] <= 7:
self._module.fail_json(msg="%s must be between 0 and 7 when using LUKS1." % self._module.params[param])
elif luks_type == 'luks2' and not 0 <= self._module.params[param] <= 31:
self._module.fail_json(msg="%s must be between 0 and 31 when using LUKS2." % self._module.params[param])
def run_module():
# available arguments/parameters that a user can pass
@@ -845,6 +977,9 @@ def run_module():
passphrase=dict(type='str', no_log=True),
new_passphrase=dict(type='str', no_log=True),
remove_passphrase=dict(type='str', no_log=True),
keyslot=dict(type='int', no_log=False),
new_keyslot=dict(type='int', no_log=False),
remove_keyslot=dict(type='int', no_log=False),
force_remove_last_key=dict(type='bool', default=False),
keysize=dict(type='int'),
label=dict(type='str'),
@@ -869,12 +1004,13 @@ def run_module():
perf_no_read_workqueue=dict(type='bool', default=False),
perf_no_write_workqueue=dict(type='bool', default=False),
persistent=dict(type='bool', default=False),
allow_discards=dict(type='bool', default=False),
)
mutually_exclusive = [
('keyfile', 'passphrase'),
('new_keyfile', 'new_passphrase'),
('remove_keyfile', 'remove_passphrase')
('remove_keyfile', 'remove_passphrase', 'remove_keyslot')
]
# seed the result dict in the object
@@ -904,6 +1040,17 @@ def run_module():
if module.params['label'] is not None and module.params['type'] == 'luks1':
module.fail_json(msg='You cannot combine type luks1 with the label option.')
if module.params['keyslot'] is not None or module.params['new_keyslot'] is not None or module.params['remove_keyslot'] is not None:
luks_type = crypt.get_luks_type(conditions.get_device_name())
if luks_type is None and module.params['type'] is not None:
luks_type = module.params['type']
for param in ['keyslot', 'new_keyslot', 'remove_keyslot']:
conditions.validate_keyslot(param, luks_type)
for param in ['new_keyslot', 'remove_keyslot']:
if module.params[param] is not None and module.params['keyfile'] is None and module.params['passphrase'] is None:
module.fail_json(msg="Removing a keyslot requires the passphrase or keyfile of another slot.")
# The conditions are in order to allow more operations in one run.
# (e.g. create luks and add a key to it)
@@ -914,6 +1061,7 @@ def run_module():
crypt.run_luks_create(conditions.device,
module.params['keyfile'],
module.params['passphrase'],
module.params['keyslot'],
module.params['keysize'],
module.params['cipher'],
module.params['hash'],
@@ -949,6 +1097,7 @@ def run_module():
module.params['perf_no_read_workqueue'],
module.params['perf_no_write_workqueue'],
module.params['persistent'],
module.params['allow_discards'],
name)
except ValueError as e:
module.fail_json(msg="luks_device error: %s" % e)
@@ -986,6 +1135,7 @@ def run_module():
module.params['passphrase'],
module.params['new_keyfile'],
module.params['new_passphrase'],
module.params['new_keyslot'],
module.params['pbkdf'])
except ValueError as e:
module.fail_json(msg="luks_device error: %s" % e)
@@ -1001,6 +1151,7 @@ def run_module():
crypt.run_luks_remove_key(conditions.device,
module.params['remove_keyfile'],
module.params['remove_passphrase'],
module.params['remove_keyslot'],
force_remove_last_key=last_key)
except ValueError as e:
module.fail_json(msg="luks_device error: %s" % e)

View File

@@ -190,7 +190,13 @@ options:
The certificate serial number may be used in a KeyRevocationList.
The serial number may be omitted for checks, but must be specified again for a new certificate.
Note: The default value set by ssh-keygen is 0."
- This option accepts an B(integer). If you want to provide serial numbers as colon-separated hex strings,
such as C(11:22:33), you need to convert them to an integer with P(community.crypto.parse_serial#filter).
type: int
seealso:
- plugin: community.crypto.parse_serial
plugin_type: filter
'''
EXAMPLES = '''

View File

@@ -55,6 +55,8 @@ seealso:
- plugin: community.crypto.openssl_csr_info
plugin_type: filter
description: A filter variant of this module.
- plugin: community.crypto.to_serial
plugin_type: filter
'''
EXAMPLES = r'''
@@ -301,6 +303,8 @@ authority_cert_serial_number:
description:
- The CSR's authority cert serial number.
- Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
returned: success
type: int
sample: 12345

View File

@@ -27,6 +27,12 @@ extends_documentation_fragment:
attributes:
check_mode:
support: full
details:
- Currently in check mode, private keys will not be (re-)generated, only the changed status is
set. This will change in community.crypto 3.0.0.
- From community.crypto 3.0.0 on, the module will ignore check mode and always behave as if
check mode is not active. If you think this breaks your use-case of this module, please
create an issue in the community.crypto repository.
diff_mode:
support: full
options:
@@ -58,7 +64,7 @@ EXAMPLES = r'''
- name: Generate an OpenSSL Certificate Signing Request with an inline CSR
community.crypto.openssl_csr:
content: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.csr') }}"
content: "{{ lookup('ansible.builtin.file', '/etc/ssl/csr/www.ansible.com.csr') }}"
privatekey_content: "{{ private_key_content }}"
common_name: www.ansible.com
register: result
@@ -146,6 +152,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.basic impo
class CertificateSigningRequestModule(object):
def __init__(self, module, module_backend):
self.check_mode = module.check_mode
self.module = module
self.module_backend = module_backend
self.changed = False
if module.params['content'] is not None:
@@ -156,6 +163,16 @@ class CertificateSigningRequestModule(object):
if self.module_backend.needs_regeneration():
if not self.check_mode:
self.module_backend.generate_csr()
else:
self.module.deprecate(
'Check mode support for openssl_csr_pipe will change in community.crypto 3.0.0'
' to behave the same as without check mode. You can get that behavior right now'
' by adding `check_mode: false` to the openssl_csr_pipe task. If you think this'
' breaks your use-case of this module, please create an issue in the'
' community.crypto repository',
version='3.0.0',
collection_name='community.crypto',
)
self.changed = True
def dump(self):

View File

@@ -193,7 +193,7 @@ class DHParameterBase(object):
"""Generate DH params."""
changed = False
# ony generate when necessary
# only generate when necessary
if self.force or not self._check_params_valid(module):
self._do_generate(module)
changed = True
@@ -341,7 +341,7 @@ class DHParameterCryptography(DHParameterBase):
try:
with open(self.path, 'rb') as f:
data = f.read()
params = self.crypto_backend.load_pem_parameters(data)
params = cryptography.hazmat.primitives.serialization.load_pem_parameters(data, backend=self.crypto_backend)
except Exception as dummy:
return False
# Check parameters

View File

@@ -24,7 +24,7 @@ description:
# Please note that the C(pyopenssl) backend has been deprecated in community.crypto x.y.0,
# and will be removed in community.crypto (x+1).0.0.
requirements:
- PyOpenSSL >= 0.15 or cryptography >= 3.0
- PyOpenSSL >= 0.15, < 23.3.0 or cryptography >= 3.0
extends_documentation_fragment:
- ansible.builtin.files
- community.crypto.attributes
@@ -302,11 +302,13 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import
MINIMAL_CRYPTOGRAPHY_VERSION = '3.0'
MINIMAL_PYOPENSSL_VERSION = '0.15'
MAXIMAL_PYOPENSSL_VERSION = '23.3.0'
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
from OpenSSL.crypto import load_pkcs12 as _load_pkcs12 # this got removed in pyOpenSSL 23.3.0
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
except (ImportError, AttributeError):
PYOPENSSL_IMP_ERR = traceback.format_exc()
@@ -711,7 +713,11 @@ 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)
can_use_pyopenssl = (
PYOPENSSL_FOUND and
PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) and
PYOPENSSL_VERSION < LooseVersion(MAXIMAL_PYOPENSSL_VERSION)
)
# If no restrictions are provided, first try cryptography, then pyOpenSSL
if (
@@ -728,14 +734,17 @@ def select_backend(module, backend):
# Success?
if backend == 'auto':
module.fail_json(msg=("Cannot detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
"cryptography (>= {0}) or PyOpenSSL (>= {1}, < {2})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION))
MINIMAL_PYOPENSSL_VERSION,
MAXIMAL_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)
msg = missing_required_lib(
'pyOpenSSL >= {0}, < {1}'.format(MINIMAL_PYOPENSSL_VERSION, MAXIMAL_PYOPENSSL_VERSION)
)
module.fail_json(msg=msg, exception=PYOPENSSL_IMP_ERR)
# module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
# version='x.0.0', collection_name='community.crypto')
return backend, PkcsPyOpenSSL(module)

View File

@@ -60,6 +60,9 @@ options:
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).
- Note that consistency checks only work for certain key types, and might depend on the
version of the cryptography library. For example, with cryptography 42.0.0 and newer
consistency of RSA keys can no longer be checked.
type: bool
default: false
version_added: 2.0.0

View File

@@ -36,6 +36,12 @@ attributes:
- This action runs completely on the controller.
check_mode:
support: full
details:
- Currently in check mode, private keys will not be (re-)generated, only the changed status is
set. This will change in community.crypto 3.0.0.
- From community.crypto 3.0.0 on, the module will ignore check mode and always behave as if
check mode is not active. If you think this breaks your use-case of this module, please
create an issue in the community.crypto repository.
diff_mode:
support: full
options:

View File

@@ -16,7 +16,7 @@ short_description: Generate an OpenSSL public key from its private key.
description:
- This module allows one to (re)generate public keys from their private keys.
- Public keys are generated in PEM or OpenSSH format. Private keys must be OpenSSL PEM keys.
OpenSSH private keys are not supported, use the M(community.crypto.openssh_keypair) module to manage these.
B(OpenSSH private keys are not supported), use the M(community.crypto.openssh_keypair) module to manage these.
- The module uses the cryptography Python library.
requirements:
- cryptography >= 1.2.3 (older versions might work as well)

View File

@@ -0,0 +1,280 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: x509_certificate_convert
short_description: Convert X.509 certificates
version_added: 2.19.0
description:
- This module allows to convert X.509 certificates between different formats.
author:
- Felix Fontein (@felixfontein)
extends_documentation_fragment:
- ansible.builtin.files
- community.crypto.attributes
- community.crypto.attributes.files
attributes:
check_mode:
support: full
diff_mode:
support: none
safe_file_operations:
support: full
options:
src_path:
description:
- Name of the file containing the X.509 certificate to convert.
- Exactly one of O(src_path) or O(src_content) must be specified.
type: path
src_content:
description:
- The content of the file containing the X.509 certificate to convert.
- This must be text. If you are not sure that the input file is PEM, you must Base64 encode
the value and set O(src_content_base64=true). You can use the
P(ansible.builtin.b64encode#filter) filter plugin for this.
- Exactly one of O(src_path) or O(src_content) must be specified.
type: str
src_content_base64:
description:
- If set to V(true) when O(src_content) is provided, the module assumes that the value
of O(src_content) is Base64 encoded.
type: bool
default: false
format:
description:
- Determines which format the destination X.509 certificate 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:
- pem
- der
required: true
strict:
description:
- If the input is a PEM file, ensure that it contains a single PEM object, that
the header and footer match, and are of type C(CERTIFICATE) or C(X509 CERTIFICATE).
type: bool
default: false
dest_path:
description:
- Name of the file in which the generated TLS/SSL X.509 certificate will be written.
type: path
required: true
backup:
description:
- Create a backup file including a timestamp so you can get
the original X.509 certificate back if you overwrote it with a new one by accident.
type: bool
default: false
seealso:
- plugin: ansible.builtin.b64encode
plugin_type: filter
- module: community.crypto.x509_certificate
- module: community.crypto.x509_certificate_pipe
- module: community.crypto.x509_certificate_info
'''
EXAMPLES = r'''
- name: Convert PEM X.509 certificate to DER format
community.crypto.x509_certificate_convert:
src_path: /etc/ssl/cert/ansible.com.pem
dest_path: /etc/ssl/cert/ansible.com.der
format: der
'''
RETURN = r'''
backup_file:
description: Name of backup file created.
returned: changed and if O(backup) is V(true)
type: str
sample: /path/to/cert.pem.2019-03-09@11:22~
'''
import base64
import os
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text
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.pem import (
PEM_START,
PEM_END_START,
PEM_END,
identify_pem_format,
split_pem_list,
extract_pem,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
OpenSSLObject,
)
def parse_certificate(input, strict=False):
input_format = 'pem' if identify_pem_format(input) else 'der'
if input_format == 'pem':
pems = split_pem_list(to_text(input))
if len(pems) > 1 and strict:
raise ValueError('The input contains {count} PEM objects, expecting only one since strict=true'.format(count=len(pems)))
pem_header_type, content = extract_pem(pems[0], strict=strict)
if strict and pem_header_type not in ('CERTIFICATE', 'X509 CERTIFICATE'):
raise ValueError('type is {type!r}, expecting CERTIFICATE or X509 CERTIFICATE'.format(type=pem_header_type))
input = base64.b64decode(content)
else:
pem_header_type = None
return input, input_format, pem_header_type
class X509CertificateConvertModule(OpenSSLObject):
def __init__(self, module):
super(X509CertificateConvertModule, self).__init__(
module.params['dest_path'],
'present',
False,
module.check_mode,
)
self.src_path = module.params['src_path']
self.src_content = module.params['src_content']
self.src_content_base64 = module.params['src_content_base64']
if self.src_content is not None:
self.input = to_bytes(self.src_content)
if self.src_content_base64:
try:
self.input = base64.b64decode(self.input)
except Exception as exc:
module.fail_json(msg='Cannot Base64 decode src_content: {exc}'.format(exc=exc))
else:
try:
with open(self.src_path, 'rb') as f:
self.input = f.read()
except Exception as exc:
module.fail_json(msg='Failure while reading file {fn}: {exc}'.format(fn=self.src_path, exc=exc))
self.format = module.params['format']
self.strict = module.params['strict']
self.wanted_pem_type = 'CERTIFICATE'
try:
self.input, self.input_format, dummy = parse_certificate(self.input, strict=self.strict)
except Exception as exc:
module.fail_json(msg='Error while parsing PEM: {exc}'.format(exc=exc))
self.backup = module.params['backup']
self.backup_file = None
module.params['path'] = self.path
self.dest_content = load_file_if_exists(self.path, module)
self.dest_content_format = None
self.dest_content_pem_type = None
if self.dest_content is not None:
try:
self.dest_content, self.dest_content_format, self.dest_content_pem_type = parse_certificate(
self.dest_content, strict=True)
except Exception:
pass
def needs_conversion(self):
if self.dest_content is None or self.dest_content_format is None:
return True
if self.dest_content_format != self.format:
return True
if self.input != self.dest_content:
return True
if self.format == 'pem' and self.dest_content_pem_type != self.wanted_pem_type:
return True
return False
def get_dest_certificate(self):
if self.format == 'der':
return self.input
data = to_bytes(base64.b64encode(self.input))
lines = [to_bytes('{0}{1}{2}'.format(PEM_START, self.wanted_pem_type, PEM_END))]
lines += [data[i:i + 64] for i in range(0, len(data), 64)]
lines.append(to_bytes('{0}{1}{2}\n'.format(PEM_END_START, self.wanted_pem_type, PEM_END)))
return b'\n'.join(lines)
def generate(self, module):
"""Do conversion."""
if self.needs_conversion():
# Convert
cert_data = self.get_dest_certificate()
if not self.check_mode:
if self.backup:
self.backup_file = module.backup_local(self.path)
write_file(module, cert_data)
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 = dict(
changed=self.changed,
)
if self.backup_file:
result['backup_file'] = self.backup_file
return result
def main():
argument_spec = dict(
src_path=dict(type='path'),
src_content=dict(type='str'),
src_content_base64=dict(type='bool', default=False),
format=dict(type='str', required=True, choices=['pem', 'der']),
strict=dict(type='bool', default=False),
dest_path=dict(type='path', required=True),
backup=dict(type='bool', default=False),
)
module = AnsibleModule(
argument_spec,
supports_check_mode=True,
add_file_common_args=True,
required_one_of=[('src_path', 'src_content')],
mutually_exclusive=[('src_path', 'src_content')],
)
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
)
try:
cert = X509CertificateConvertModule(module)
cert.generate(module)
result = cert.dump()
module.exit_json(**result)
except OpenSSLObjectError as exc:
module.fail_json(msg=to_native(exc))
if __name__ == '__main__':
main()

View File

@@ -52,7 +52,7 @@ options:
description:
- A dict of names mapping to time specifications. Every time specified here
will be checked whether the certificate is valid at this point. See the
RV(valid_at) return value for informations on the result.
RV(valid_at) return value for information on the result.
- 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
@@ -77,6 +77,8 @@ seealso:
- plugin: community.crypto.x509_certificate_info
plugin_type: filter
description: A filter variant of this module.
- plugin: community.crypto.to_serial
plugin_type: filter
'''
EXAMPLES = r'''
@@ -330,7 +332,10 @@ signature_algorithm:
type: str
sample: sha256WithRSAEncryption
serial_number:
description: The certificate's serial number.
description:
- The certificate's serial number.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
returned: success
type: int
sample: 1234
@@ -374,6 +379,8 @@ authority_cert_serial_number:
description:
- The certificate's authority cert serial number.
- Is V(none) if the C(AuthorityKeyIdentifier) extension is not present.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
returned: success
type: int
sample: 12345
@@ -399,14 +406,18 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.basic impo
OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
get_relative_time_option,
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
select_backend,
)
from ansible_collections.community.crypto.plugins.module_utils.time import (
get_relative_time_option,
)
def main():
module = AnsibleModule(
@@ -444,7 +455,7 @@ def main():
module.fail_json(
msg='The value for valid_at.{0} must be of type string (got {1})'.format(k, type(v))
)
valid_at[k] = get_relative_time_option(v, 'valid_at.{0}'.format(k))
valid_at[k] = get_relative_time_option(v, 'valid_at.{0}'.format(k), with_timezone=CRYPTOGRAPHY_TIMEZONE)
try:
result = module_backend.get_info(der_support_enabled=module.params['content'] is None)

View File

@@ -32,6 +32,12 @@ extends_documentation_fragment:
attributes:
check_mode:
support: full
details:
- Currently in check mode, private keys will not be (re-)generated, only the changed status is
set. This will change in community.crypto 3.0.0.
- From community.crypto 3.0.0 on, the module will ignore check mode and always behave as if
check mode is not active. If you think this breaks your use-case of this module, please
create an issue in the community.crypto repository.
diff_mode:
support: full
options:
@@ -71,8 +77,8 @@ EXAMPLES = r'''
- name: (1/2) Generate an OpenSSL Certificate with the CSR provided inline
community.crypto.x509_certificate_pipe:
provider: ownca
content: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.crt') }}"
csr_content: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.csr') }}"
content: "{{ lookup('ansible.builtin.file', '/etc/ssl/csr/www.ansible.com.crt') }}"
csr_content: "{{ lookup('ansible.builtin.file', '/etc/ssl/csr/www.ansible.com.csr') }}"
ownca_cert: /path/to/ca_cert.crt
ownca_privatekey: /path/to/ca_cert.key
ownca_privatekey_passphrase: hunter2
@@ -154,6 +160,7 @@ class GenericCertificate(object):
"""Retrieve a certificate using the given module backend."""
def __init__(self, module, module_backend):
self.check_mode = module.check_mode
self.module = module
self.module_backend = module_backend
self.changed = False
if module.params['content'] is not None:
@@ -163,6 +170,16 @@ class GenericCertificate(object):
if self.module_backend.needs_regeneration():
if not self.check_mode:
self.module_backend.generate_certificate()
else:
self.module.deprecate(
'Check mode support for x509_certificate_pipe will change in community.crypto 3.0.0'
' to behave the same as without check mode. You can get that behavior right now'
' by adding `check_mode: false` to the x509_certificate_pipe task. If you think this'
' breaks your use-case of this module, please create an issue in the'
' community.crypto repository',
version='3.0.0',
collection_name='community.crypto',
)
self.changed = True
def dump(self, check_mode=False):

View File

@@ -164,6 +164,21 @@ options:
type: str
default: sha256
serial_numbers:
description:
- This option determines which values will be accepted for O(revoked_certificates[].serial_number).
- If set to V(integer) (default), serial numbers are assumed to be integers, for example V(66223).
(This example value is equivalent to the hex octet string V(01:02:AF).)
- If set to V(hex-octets), serial numbers are assumed to be colon-separated hex octet strings,
for example V(01:02:AF).
(This example value is equivalent to the integer V(66223).)
type: str
choices:
- integer
- hex-octets
default: integer
version_added: 2.18.0
revoked_certificates:
description:
- List of certificates to be revoked.
@@ -193,7 +208,13 @@ options:
- Mutually exclusive with O(revoked_certificates[].path) and
O(revoked_certificates[].content). One of these three options must
be specified.
type: int
- This option accepts integers or hex octet strings, depending on the value
of O(serial_numbers).
- If O(serial_numbers=integer), integers such as V(66223) must be provided.
- If O(serial_numbers=hex-octets), strings such as V(01:02:AF) must be provided.
- You can use the filters P(community.crypto.parse_serial#filter) and
P(community.crypto.to_serial#filter) to convert these two representations.
type: raw
revocation_date:
description:
- The point in time the certificate was revoked.
@@ -271,6 +292,12 @@ options:
notes:
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
- Date specified should be UTC. Minutes and seconds are mandatory.
seealso:
- plugin: community.crypto.parse_serial
plugin_type: filter
- plugin: community.crypto.to_serial
plugin_type: filter
'''
EXAMPLES = r'''
@@ -356,7 +383,10 @@ revoked_certificates:
elements: dict
contains:
serial_number:
description: Serial number of the certificate.
description:
- Serial number of the certificate.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
type: int
sample: 1234
revocation_date:
@@ -420,7 +450,9 @@ import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible.module_utils.common.validation import check_type_int, check_type_str
from ansible_collections.community.crypto.plugins.module_utils.serial import parse_serial
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.io import (
@@ -438,11 +470,11 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
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_TIMEZONE,
cryptography_decode_name,
cryptography_get_name,
cryptography_key_needs_digest_for_signing,
@@ -452,11 +484,17 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import (
CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE,
REVOCATION_REASON_MAP,
TIMESTAMP_FORMAT,
cryptography_decode_revoked_certificate,
cryptography_dump_revoked,
cryptography_get_signature_algorithm_oid_from_crl,
get_next_update,
get_last_update,
set_next_update,
set_last_update,
set_revocation_date,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
@@ -467,6 +505,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
get_crl_info,
)
from ansible_collections.community.crypto.plugins.module_utils.time import (
get_relative_time_option,
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
CRYPTOGRAPHY_IMP_ERR = None
@@ -509,6 +551,7 @@ class CRL(OpenSSLObject):
self.ignore_timestamps = module.params['ignore_timestamps']
self.return_content = module.params['return_content']
self.name_encoding = module.params['name_encoding']
self.serial_numbers_format = module.params['serial_numbers']
self.crl_content = None
self.privatekey_path = module.params['privatekey_path']
@@ -527,13 +570,15 @@ class CRL(OpenSSLObject):
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')
self.last_update = get_relative_time_option(module.params['last_update'], 'last_update', with_timezone=CRYPTOGRAPHY_TIMEZONE)
self.next_update = get_relative_time_option(module.params['next_update'], 'next_update', with_timezone=CRYPTOGRAPHY_TIMEZONE)
self.digest = select_message_digest(module.params['digest'])
if self.digest is None:
raise CRLError('The digest "{0}" is not supported'.format(module.params['digest']))
self.module = module
self.revoked_certificates = []
for i, rc in enumerate(module.params['revoked_certificates']):
result = {
@@ -565,14 +610,15 @@ class CRL(OpenSSLObject):
)
else:
# Specify serial_number (and potentially issuer) directly
result['serial_number'] = rc['serial_number']
result['serial_number'] = self._parse_serial_number(rc['serial_number'], i)
# All other options
if rc['issuer']:
result['issuer'] = [cryptography_get_name(issuer, 'issuer') for issuer in rc['issuer']]
result['issuer_critical'] = rc['issuer_critical']
result['revocation_date'] = get_relative_time_option(
rc['revocation_date'],
path_prefix + 'revocation_date'
path_prefix + 'revocation_date',
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
if rc['reason']:
result['reason'] = REVOCATION_REASON_MAP[rc['reason']]
@@ -580,13 +626,12 @@ class CRL(OpenSSLObject):
if rc['invalidity_date']:
result['invalidity_date'] = get_relative_time_option(
rc['invalidity_date'],
path_prefix + 'invalidity_date'
path_prefix + 'invalidity_date',
with_timezone=CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE,
)
result['invalidity_date_critical'] = rc['invalidity_date_critical']
self.revoked_certificates.append(result)
self.module = module
self.backup = module.params['backup']
self.backup_file = None
@@ -620,6 +665,25 @@ class CRL(OpenSSLObject):
self.diff_after = self.diff_before = self._get_info(data)
def _parse_serial_number(self, value, index):
if self.serial_numbers_format == 'integer':
try:
return check_type_int(value)
except TypeError as exc:
self.module.fail_json(msg='Error while parsing revoked_certificates[{idx}].serial_number as an integer: {exc}'.format(
idx=index + 1,
exc=to_native(exc),
))
if self.serial_numbers_format == 'hex-octets':
try:
return parse_serial(check_type_str(value))
except (TypeError, ValueError) as exc:
self.module.fail_json(msg='Error while parsing revoked_certificates[{idx}].serial_number as an colon-separated hex octet string: {exc}'.format(
idx=index + 1,
exc=to_native(exc),
))
raise RuntimeError('Unexpected value %s of serial_numbers' % (self.serial_numbers_format, ))
def _get_info(self, data):
if data is None:
return dict()
@@ -679,9 +743,9 @@ class CRL(OpenSSLObject):
if self.crl is None:
return False
if self.last_update != self.crl.last_update and not self.ignore_timestamps:
if self.last_update != get_last_update(self.crl) and not self.ignore_timestamps:
return False
if self.next_update != self.crl.next_update and not self.ignore_timestamps:
if self.next_update != get_next_update(self.crl) and not self.ignore_timestamps:
return False
if cryptography_key_needs_digest_for_signing(self.privatekey):
if self.crl.signature_hash_algorithm is None or self.digest.name != self.crl.signature_hash_algorithm.name:
@@ -728,8 +792,8 @@ class CRL(OpenSSLObject):
except ValueError as e:
raise CRLError(e)
crl = crl.last_update(self.last_update)
crl = crl.next_update(self.next_update)
crl = set_last_update(crl, self.last_update)
crl = set_next_update(crl, self.next_update)
if self.update and self.crl:
new_entries = set([self._compress_entry(entry) for entry in self.revoked_certificates])
@@ -740,7 +804,7 @@ class CRL(OpenSSLObject):
for entry in self.revoked_certificates:
revoked_cert = RevokedCertificateBuilder()
revoked_cert = revoked_cert.serial_number(entry['serial_number'])
revoked_cert = revoked_cert.revocation_date(entry['revocation_date'])
revoked_cert = set_revocation_date(revoked_cert, entry['revocation_date'])
if entry['issuer'] is not None:
revoked_cert = revoked_cert.add_extension(
x509.CertificateIssuer(entry['issuer']),
@@ -824,8 +888,8 @@ class CRL(OpenSSLObject):
for entry in self.revoked_certificates:
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)
result['last_update'] = get_last_update(self.crl).strftime(TIMESTAMP_FORMAT)
result['next_update'] = get_next_update(self.crl).strftime(TIMESTAMP_FORMAT)
result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl))
issuer = []
for attribute in self.crl.issuer:
@@ -885,7 +949,7 @@ def main():
options=dict(
path=dict(type='path'),
content=dict(type='str'),
serial_number=dict(type='int'),
serial_number=dict(type='raw'),
revocation_date=dict(type='str', default='+0s'),
issuer=dict(type='list', elements='str'),
issuer_critical=dict(type='bool', default=False),
@@ -905,6 +969,7 @@ def main():
mutually_exclusive=[['path', 'content', 'serial_number']],
),
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
serial_numbers=dict(type='str', default='integer', choices=['integer', 'hex-octets']),
),
required_if=[
('state', 'present', ['privatekey_path', 'privatekey_content'], True),

View File

@@ -53,6 +53,8 @@ seealso:
- plugin: community.crypto.x509_crl_info
plugin_type: filter
description: A filter variant of this module.
- plugin: community.crypto.to_serial
plugin_type: filter
'''
EXAMPLES = r'''
@@ -118,7 +120,10 @@ revoked_certificates:
elements: dict
contains:
serial_number:
description: Serial number of the certificate.
description:
- Serial number of the certificate.
- This return value is an B(integer). If you need the serial numbers as a colon-separated hex string,
such as C(11:22:33), you need to convert it to that form with P(community.crypto.to_serial#filter).
type: int
sample: 1234
revocation_date:

View File

@@ -0,0 +1,10 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
azp/generic/1
azp/posix/1
cloud/acme
# For some reason connecting to helper containers does not work on the Alpine VMs
skip/alpine

View File

@@ -0,0 +1,8 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
dependencies:
- setup_acme
- setup_remote_tmp_dir

View File

@@ -0,0 +1,154 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- vars:
certificate_name: cert-1
subject_alt_name: DNS:example.com
account_email: example@example.org
block:
- name: Generate account key
openssl_privatekey:
path: "{{ remote_tmp_dir }}/account-ec256.pem"
type: ECC
curve: secp256r1
force: true
- name: Create cert private key
openssl_privatekey:
path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key"
type: ECC
curve: secp256r1
force: true
- name: Create cert CSR
openssl_csr:
path: "{{ remote_tmp_dir }}/{{ certificate_name }}.csr"
privatekey_path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key"
subject_alt_name: "{{ subject_alt_name }}"
- name: Start process of obtaining certificate
acme_certificate:
select_crypto_backend: "{{ select_crypto_backend }}"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
modify_account: true
csr: "{{ remote_tmp_dir }}/{{ certificate_name }}.csr"
dest: "{{ remote_tmp_dir }}/{{ certificate_name }}.pem"
challenge: http-01
force: true
terms_agreed: true
account_email: "{{ account_email }}"
register: certificate_data
- name: Inspect order
acme_inspect:
acme_directory: https://{{ acme_host }}:14000/dir
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
account_uri: "{{ certificate_data.account_uri }}"
url: "{{ certificate_data.order_uri }}"
method: get
register: order_1
- name: Show order
debug:
var: order_1.output_json
- name: Deactivate order (check mode)
acme_certificate_deactivate_authz:
acme_directory: https://{{ acme_host }}:14000/dir
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
account_uri: "{{ certificate_data.account_uri }}"
order_uri: "{{ certificate_data.order_uri }}"
check_mode: true
register: deactivate_1
- name: Inspect order again
acme_inspect:
acme_directory: https://{{ acme_host }}:14000/dir
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
account_uri: "{{ certificate_data.account_uri }}"
url: "{{ certificate_data.order_uri }}"
method: get
register: order_2
- name: Show order
debug:
var: order_2.output_json
- name: Deactivate order
acme_certificate_deactivate_authz:
acme_directory: https://{{ acme_host }}:14000/dir
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
account_uri: "{{ certificate_data.account_uri }}"
order_uri: "{{ certificate_data.order_uri }}"
register: deactivate_2
- name: Inspect order again
acme_inspect:
acme_directory: https://{{ acme_host }}:14000/dir
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
account_uri: "{{ certificate_data.account_uri }}"
url: "{{ certificate_data.order_uri }}"
method: get
register: order_3
- name: Show order
debug:
var: order_3.output_json
- name: Deactivate order (check mode, idempotent)
acme_certificate_deactivate_authz:
acme_directory: https://{{ acme_host }}:14000/dir
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
account_uri: "{{ certificate_data.account_uri }}"
order_uri: "{{ certificate_data.order_uri }}"
check_mode: true
register: deactivate_3
- name: Inspect order again
acme_inspect:
acme_directory: https://{{ acme_host }}:14000/dir
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
account_uri: "{{ certificate_data.account_uri }}"
url: "{{ certificate_data.order_uri }}"
method: get
register: order_4
- name: Show order
debug:
var: order_4.output_json
- name: Deactivate order (idempotent)
acme_certificate_deactivate_authz:
acme_directory: https://{{ acme_host }}:14000/dir
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
account_uri: "{{ certificate_data.account_uri }}"
order_uri: "{{ certificate_data.order_uri }}"
register: deactivate_4
- name: Inspect order again
acme_inspect:
acme_directory: https://{{ acme_host }}:14000/dir
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
account_uri: "{{ certificate_data.account_uri }}"
url: "{{ certificate_data.order_uri }}"
method: get
register: order_5
- name: Show order
debug:
var: order_5.output_json

View File

@@ -0,0 +1,40 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
- block:
- name: Running tests with OpenSSL backend
include_tasks: impl.yml
vars:
select_crypto_backend: openssl
- import_tasks: ../tests/validate.yml
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
when: openssl_version.stdout is version('1.0.0', '>=')
- name: Remove output directory
file:
path: "{{ remote_tmp_dir }}"
state: absent
- name: Re-create output directory
file:
path: "{{ remote_tmp_dir }}"
state: directory
- block:
- name: Running tests with cryptography backend
include_tasks: impl.yml
vars:
select_crypto_backend: cryptography
- import_tasks: ../tests/validate.yml
when: cryptography_version.stdout is version('1.5', '>=')

View File

@@ -0,0 +1,17 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Checks
assert:
that:
- order_1.output_json.status == 'pending'
- deactivate_1 is changed
- order_2.output_json.status == 'pending'
- deactivate_2 is changed
- order_3.output_json.status == 'deactivated'
- deactivate_3 is not changed
- order_4.output_json.status == 'deactivated'
- deactivate_4 is not changed
- order_5.output_json.status == 'deactivated'

View File

@@ -0,0 +1,10 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
azp/generic/1
azp/posix/1
cloud/acme
# For some reason connecting to helper containers does not work on the Alpine VMs
skip/alpine

View File

@@ -0,0 +1,8 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
dependencies:
- setup_acme
- setup_remote_tmp_dir

View File

@@ -0,0 +1,145 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
## SET UP ACCOUNT KEYS ########################################################################
- block:
- name: Generate account keys
openssl_privatekey:
path: "{{ remote_tmp_dir }}/{{ item.name }}.pem"
type: "{{ item.type }}"
size: "{{ item.size | default(omit) }}"
curve: "{{ item.curve | default(omit) }}"
force: true
loop: "{{ account_keys }}"
vars:
account_keys:
- name: account-ec256
type: ECC
curve: secp256r1
## CREATE ACCOUNTS AND OBTAIN CERTIFICATES ####################################################
- name: Obtain cert 1
include_tasks: obtain-cert.yml
vars:
certgen_title: Certificate 1 for renewal check
certificate_name: cert-1
key_type: rsa
rsa_bits: "{{ default_rsa_key_size }}"
subject_alt_name: "DNS:example.com"
subject_alt_name_critical: false
account_key: account-ec256
challenge: http-01
modify_account: true
deactivate_authzs: false
force: true
remaining_days: "{{ omit }}"
terms_agreed: true
account_email: "example@example.org"
## OBTAIN CERTIFICATE INFOS ###################################################################
- name: Dump OpenSSL x509 info
command:
cmd: openssl x509 -in {{ remote_tmp_dir }}/cert-1.pem -noout -text
- name: Obtain certificate information
x509_certificate_info:
path: "{{ remote_tmp_dir }}/cert-1.pem"
register: cert_1_info
- name: Read certificate
slurp:
src: '{{ remote_tmp_dir }}/cert-1.pem'
register: slurp_cert_1
- name: Obtain certificate information (1/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
# Certificate is valid for ~1826 days
register: cert_1_renewal_1
- name: Obtain certificate information (2/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
# Certificate is valid for ~1826 days
remaining_days: 1000
remaining_percentage: 0.5
register: cert_1_renewal_2
- name: Obtain certificate information (3/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_content: "{{ slurp_cert_1.content | b64decode }}"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
now: +1800d
# Certificate is valid for ~26 days
register: cert_1_renewal_3
- name: Obtain certificate information (4/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
now: +1800d
# Certificate is valid for ~26 days
remaining_days: 30
remaining_percentage: 0.1
register: cert_1_renewal_4
- name: Obtain certificate information (5/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
now: +1800d
# Certificate is valid for ~26 days
remaining_days: 30
remaining_percentage: 0.01
register: cert_1_renewal_5
- name: Obtain certificate information (6/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
now: +1800d
# Certificate is valid for ~26 days
remaining_days: 10
remaining_percentage: 0.03
register: cert_1_renewal_6
- name: Obtain certificate information (7/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
now: +1830d
# Certificate is no longer valid
register: cert_1_renewal_7
- name: Obtain certificate information (8/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
now: +1830d
# Certificate is no longer valid
register: cert_1_renewal_8
- name: Obtain certificate information (9/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-does-not-exist.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
# Certificate is no longer valid
register: cert_1_renewal_9

View File

@@ -0,0 +1,40 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
- block:
- name: Running tests with OpenSSL backend
include_tasks: impl.yml
vars:
select_crypto_backend: openssl
- import_tasks: ../tests/validate.yml
# Old 0.9.8 versions have insufficient CLI support for signing with EC keys
when: openssl_version.stdout is version('1.0.0', '>=')
- name: Remove output directory
file:
path: "{{ remote_tmp_dir }}"
state: absent
- name: Re-create output directory
file:
path: "{{ remote_tmp_dir }}"
state: directory
- block:
- name: Running tests with cryptography backend
include_tasks: impl.yml
vars:
select_crypto_backend: cryptography
- import_tasks: ../tests/validate.yml
when: cryptography_version.stdout is version('1.5', '>=')

View File

@@ -0,0 +1 @@
../../setup_acme/tasks/obtain-cert.yml

View File

@@ -0,0 +1,47 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Validate results
assert:
that:
- cert_1_renewal_1.should_renew == false
- cert_1_renewal_1.msg == 'The certificate is still valid and no condition was reached'
- cert_1_renewal_1.supports_ari == supports_ari
- cert_1_renewal_1.cert_id is string or not can_have_cert_id
- cert_1_renewal_2.should_renew == false
- cert_1_renewal_2.msg == 'The certificate is still valid and no condition was reached'
- cert_1_renewal_2.supports_ari == supports_ari
- cert_1_renewal_2.cert_id is string or not can_have_cert_id
- cert_1_renewal_3.should_renew == false
- cert_1_renewal_3.msg == 'The certificate is still valid and no condition was reached'
- cert_1_renewal_3.supports_ari == supports_ari
- cert_1_renewal_3.cert_id is string or not can_have_cert_id
- cert_1_renewal_4.should_renew == true
- cert_1_renewal_4.msg == 'The certificate expires in 25 days'
- cert_1_renewal_4.supports_ari == supports_ari
- cert_1_renewal_4.cert_id is string or not can_have_cert_id
- cert_1_renewal_5.should_renew == true
- cert_1_renewal_5.msg == 'The certificate expires in 25 days'
- cert_1_renewal_5.supports_ari == supports_ari
- cert_1_renewal_5.cert_id is string or not can_have_cert_id
- cert_1_renewal_6.should_renew == true
- cert_1_renewal_6.msg.startswith("The remaining percentage 3.0% of the certificate's lifespan was reached on ")
- cert_1_renewal_6.supports_ari == supports_ari
- cert_1_renewal_6.cert_id is string or not can_have_cert_id
- cert_1_renewal_7.should_renew == true
- cert_1_renewal_7.msg == 'The certificate has already expired'
- cert_1_renewal_7.supports_ari == false
- cert_1_renewal_7.cert_id is string or not can_have_cert_id
- cert_1_renewal_8.should_renew == true
- cert_1_renewal_8.msg == 'No certificate was specified'
- cert_1_renewal_8.supports_ari == false
- cert_1_renewal_8.cert_id is not defined
- cert_1_renewal_9.should_renew == true
- cert_1_renewal_9.msg == 'The certificate file does not exist'
- cert_1_renewal_9.supports_ari == false
- cert_1_renewal_9.cert_id is not defined
vars:
can_have_cert_id: cert_1_info.authority_key_identifier is string
supports_ari: false

View File

@@ -9,7 +9,7 @@
####################################################################
- block:
- name: Generate ECC256 accoun keys
- name: Generate ECC256 account keys
openssl_privatekey:
path: "{{ remote_tmp_dir }}/account-ec256.pem"
type: ECC

View File

@@ -28,6 +28,7 @@
acme_version: 2
validate_certs: false
method: directory-only
select_crypto_backend: "{{ select_crypto_backend }}"
register: directory
- debug: var=directory
@@ -40,6 +41,7 @@
url: "{{ directory.directory.newAccount}}"
method: post
content: '{"termsOfServiceAgreed":true}'
select_crypto_backend: "{{ select_crypto_backend }}"
register: account_creation
# account_creation.headers.location contains the account URI
# if creation was successful
@@ -54,6 +56,7 @@
account_uri: "{{ account_creation.headers.location }}"
url: "{{ account_creation.headers.location }}"
method: get
select_crypto_backend: "{{ select_crypto_backend }}"
register: account_get
- debug: var=account_get
@@ -67,6 +70,7 @@
url: "{{ account_creation.headers.location }}"
method: post
content: '{{ account_info | to_json }}'
select_crypto_backend: "{{ select_crypto_backend }}"
vars:
account_info:
# For valid values, see
@@ -86,6 +90,7 @@
url: "{{ directory.directory.newOrder }}"
method: post
content: '{{ create_order | to_json }}'
select_crypto_backend: "{{ select_crypto_backend }}"
vars:
create_order:
# For valid values, see
@@ -108,6 +113,7 @@
account_uri: "{{ account_creation.headers.location }}"
url: "{{ new_order.headers.location }}"
method: get
select_crypto_backend: "{{ select_crypto_backend }}"
register: order
- debug: var=order
@@ -120,6 +126,7 @@
account_uri: "{{ account_creation.headers.location }}"
url: "{{ item }}"
method: get
select_crypto_backend: "{{ select_crypto_backend }}"
loop: "{{ order.output_json.authorizations }}"
register: authz
- debug: var=authz
@@ -133,6 +140,7 @@
account_uri: "{{ account_creation.headers.location }}"
url: "{{ (item.challenges | selectattr('type', 'equalto', 'http-01') | list)[0].url }}"
method: get
select_crypto_backend: "{{ select_crypto_backend }}"
register: http01challenge
loop: "{{ authz.results | map(attribute='output_json') | list }}"
- debug: var=http01challenge
@@ -147,6 +155,7 @@
url: "{{ item.url }}"
method: post
content: '{}'
select_crypto_backend: "{{ select_crypto_backend }}"
register: activation
loop: "{{ http01challenge.results | map(attribute='output_json') | list }}"
- debug: var=activation
@@ -160,6 +169,7 @@
account_uri: "{{ account_creation.headers.location }}"
url: "{{ item.url }}"
method: get
select_crypto_backend: "{{ select_crypto_backend }}"
register: validation_result
loop: "{{ http01challenge.results | map(attribute='output_json') | list }}"
until: "validation_result.output_json.status not in ['pending', 'processing']"

View File

@@ -0,0 +1,5 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
azp/posix/2

View File

@@ -0,0 +1,62 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Test parse_serial filter
assert:
that:
- >-
'0' | community.crypto.parse_serial == 0
- >-
'00' | community.crypto.parse_serial == 0
- >-
'000' | community.crypto.parse_serial == 0
- >-
'ff' | community.crypto.parse_serial == 255
- >-
'0ff' | community.crypto.parse_serial == 255
- >-
'1:0' | community.crypto.parse_serial == 256
- >-
'1:2:3' | community.crypto.parse_serial == 66051
- name: "Test error 1: empty string"
debug:
msg: >-
{{ '' | community.crypto.parse_serial }}
ignore_errors: true
register: error_1
- name: "Test error 2: invalid type"
debug:
msg: >-
{{ [] | community.crypto.parse_serial }}
ignore_errors: true
register: error_2
- name: "Test error 3: invalid values (range)"
debug:
msg: >-
{{ '100' | community.crypto.parse_serial }}
ignore_errors: true
register: error_3
- name: "Test error 4: invalid values (digits)"
debug:
msg: >-
{{ 'abcdefg' | community.crypto.parse_serial }}
ignore_errors: true
register: error_4
- name: Validate errors
assert:
that:
- >-
error_1 is failed and "The 1st part '' is not a hexadecimal number in range [0, 255]: invalid literal" in error_1.msg
- >-
error_2 is failed and "The input for the community.crypto.parse_serial filter must be a string; got " in error_2.msg
- >-
error_3 is failed and "The 1st part '100' is not a hexadecimal number in range [0, 255]: the value is not in range [0, 255]" in error_3.msg
- >-
error_4 is failed and "The 1st part 'abcdefg' is not a hexadecimal number in range [0, 255]: invalid literal" in error_4.msg

View File

@@ -0,0 +1,5 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
azp/posix/2

View File

@@ -0,0 +1,35 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Test to_serial filter
assert:
that:
- 0 | community.crypto.to_serial == '00'
- 1 | community.crypto.to_serial == '01'
- 255 | community.crypto.to_serial == 'FF'
- 256 | community.crypto.to_serial == '01:00'
- 65536 | community.crypto.to_serial == '01:00:00'
- name: "Test error 1: negative number"
debug:
msg: >-
{{ (-1) | community.crypto.to_serial }}
ignore_errors: true
register: error_1
- name: "Test error 2: invalid type"
debug:
msg: >-
{{ [] | community.crypto.to_serial }}
ignore_errors: true
register: error_2
- name: Validate error
assert:
that:
- >-
error_1 is failed and "The input for the community.crypto.to_serial filter must not be negative" in error_1.msg
- >-
error_2 is failed and "The input for the community.crypto.to_serial filter must be an integer; got " in error_2.msg

View File

@@ -10,6 +10,8 @@
- set_fact:
skip_tests: false
has_get_certificate_chain: >-
{{ ansible_facts.python_version is version('3.10.0', '>=') }}
- block:

View File

@@ -71,7 +71,11 @@
- result is not changed
- result is failed
# We got the expected error message
- "'The handshake operation timed out' in result.msg or 'unknown protocol' in result.msg or 'wrong version number' in result.msg"
- >-
'The handshake operation timed out' in result.msg
or 'unknown protocol' in result.msg
or 'wrong version number' in result.msg
or 'record layer failure' in result.msg
- name: Test timeout option
get_certificate:
@@ -119,6 +123,7 @@
port: 443
select_crypto_backend: "{{ select_crypto_backend }}"
asn1_base64: true
get_certificate_chain: "{{ has_get_certificate_chain }}"
register: result
- assert:
@@ -126,6 +131,30 @@
- result is not changed
- result is not failed
- name: Read CA cert
slurp:
src: '{{ remote_tmp_dir }}/temp.pem'
register: cacert
when: has_get_certificate_chain
- name: Validate get_certificate_chain=true results
assert:
that:
- result.verified_chain is sequence
- result.unverified_chain is sequence
- result.verified_chain[0] == result.cert
- result.unverified_chain[0] == result.cert
- result.verified_chain[-1] == cacert.content | b64decode
- result.verified_chain == result.unverified_chain + [cacert.content | b64decode]
when: has_get_certificate_chain
- name: Validate get_certificate_chain=false results
assert:
that:
- result.verified_chain is undefined
- result.unverified_chain is undefined
when: not has_get_certificate_chain
- name: Generate bogus CA privatekey
openssl_privatekey:
path: '{{ remote_tmp_dir }}/bogus_ca.key'

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